From fc6b365704722292aef1116f0f2905e6f1ce7998 Mon Sep 17 00:00:00 2001 From: quantal Date: Mon, 25 May 2026 12:40:32 +0800 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96token=E8=AE=A1?= =?UTF-8?q?=E9=87=8F=E8=BE=93=E5=87=BA=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/shared/columns/usageColumns.tsx | 41 +++++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/web/src/shared/columns/usageColumns.tsx b/web/src/shared/columns/usageColumns.tsx index f8cd56ab..b61ce651 100644 --- a/web/src/shared/columns/usageColumns.tsx +++ b/web/src/shared/columns/usageColumns.tsx @@ -294,12 +294,31 @@ function isTotalMetric(metric: UsageMetric) { return metricMatches(metric, ['total_tokens', 'total_token', 'total']); } -function formatMetricValue(metric: UsageMetric): string { - const value = metricNumber(metric.value); - const formatted = Number.isInteger(value) +function isReasoningMetric(metric: UsageMetric) { + return metricMatches(metric, ['reasoning_output_tokens', 'reasoning_tokens', 'reasoning_token']); +} + +function isOutputMetric(metric: UsageMetric) { + return metricMatches(metric, ['output_tokens', 'output_token', 'completion_tokens', 'completion_token']); +} + +function isTokenUnit(unit?: string) { + const normalized = normalizeUsageKey(unit); + return normalized === 'token' || normalized === 'tokens'; +} + +function formatMetricNumber(value: number): string { + return Number.isInteger(value) ? value.toLocaleString() : value.toLocaleString(undefined, { maximumFractionDigits: 4 }); - return metric.unit ? `${formatted} ${metric.unit}` : formatted; +} + +function formatMetricValue(metric: UsageMetric): string { + const value = metricNumber(metric.value); + const formatted = formatMetricNumber(value); + const unit = metric.unit?.trim(); + if (!unit || isTokenUnit(unit)) return formatted; + return `${formatted} ${unit}`; } function metricColor(metric: UsageMetric, index: number): string | undefined { @@ -392,8 +411,13 @@ function buildCostDetailContext(row: UsageLogResp, adminView: boolean) { function GenericMetricDetail({ row, t }: { row: UsageRow; t: TFunction }) { const allMetrics = rowMetrics(row); const hasSDKMetrics = (row.usage_metrics?.length ?? 0) > 0; + const reasoningTokens = metricValue(allMetrics, ['reasoning_output_tokens', 'reasoning_tokens', 'reasoning_token']) + ?? (row as Partial).reasoning_output_tokens + ?? 0; const metrics = allMetrics.filter((metric) => ( - !isTotalMetric(metric) && (metricNumber(metric.value) > 0 || !hasSDKMetrics) + !isTotalMetric(metric) + && !isReasoningMetric(metric) + && (metricNumber(metric.value) > 0 || !hasSDKMetrics || (isOutputMetric(metric) && reasoningTokens > 0)) )); const totalMetric = allMetrics.find(isTotalMetric); const tokenTotal = @@ -407,7 +431,12 @@ function GenericMetricDetail({ row, t }: { row: UsageRow; t: TFunction }) { 0 ? ( + + (推理 {formatMetricNumber(reasoningTokens)}) + {formatMetricValue(metric)} + + ) : formatMetricValue(metric)} color={metricColor(metric, index)} /> ))} From 6fd08fa22881be320b9f7bb781466731d6b0a413 Mon Sep 17 00:00:00 2001 From: quantal Date: Wed, 27 May 2026 16:03:35 +0800 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BB=8E=20Git?= =?UTF-8?q?Hub=20=E5=AE=89=E8=A3=85=E6=8C=87=E5=AE=9A=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/pluginadmin/service.go | 7 +- .../internal/app/pluginadmin/service_test.go | 2 +- backend/internal/app/pluginadmin/types.go | 2 +- backend/internal/plugin/manager_install.go | 129 ++++++++++++++---- backend/internal/plugin/manager_test.go | 46 +++++++ backend/internal/server/dto/plugin.go | 3 +- .../server/handler/plugin_handler_routes.go | 2 +- web/src/i18n/en.json | 4 + web/src/i18n/zh.json | 4 + web/src/pages/admin/PluginsPage.tsx | 116 +++++++++++++--- web/src/shared/api/plugins.ts | 4 +- 11 files changed, 266 insertions(+), 53 deletions(-) diff --git a/backend/internal/app/pluginadmin/service.go b/backend/internal/app/pluginadmin/service.go index 565c97a3..d7bd4a86 100644 --- a/backend/internal/app/pluginadmin/service.go +++ b/backend/internal/app/pluginadmin/service.go @@ -101,17 +101,20 @@ func (s *Service) Upload(ctx context.Context, name string, binary []byte) error } // InstallFromGithub 从 GitHub 安装插件。 -func (s *Service) InstallFromGithub(ctx context.Context, repo string) error { +func (s *Service) InstallFromGithub(ctx context.Context, repo, version string) error { logger := sdk.LoggerFromContext(ctx) - if err := s.manager.InstallFromGithub(ctx, repo); err != nil { + version = strings.TrimSpace(version) + if err := s.manager.InstallFromGithub(ctx, repo, version); err != nil { logger.Error("plugin_admin_uploaded_failed", "repo", repo, + "version", version, "source", "github", sdk.LogFieldError, err) return err } logger.Info("plugin_admin_uploaded", "repo", repo, + "version", version, "source", "github") return nil } diff --git a/backend/internal/app/pluginadmin/service_test.go b/backend/internal/app/pluginadmin/service_test.go index 64b6cec1..07aaec8d 100644 --- a/backend/internal/app/pluginadmin/service_test.go +++ b/backend/internal/app/pluginadmin/service_test.go @@ -58,7 +58,7 @@ func (s pluginAdminManagerStub) GetAllPluginMeta() []plugin.PluginMeta { return append([]plugin.PluginMeta(nil), s.allMeta...) } func (s pluginAdminManagerStub) InstallFromBinary(context.Context, string, []byte) error { return nil } -func (s pluginAdminManagerStub) InstallFromGithub(context.Context, string) error { return nil } +func (s pluginAdminManagerStub) InstallFromGithub(context.Context, string, string) error { return nil } func (s pluginAdminManagerStub) Uninstall(context.Context, string) error { return nil } func (s pluginAdminManagerStub) ReloadDev(context.Context, string) error { return nil } func (s pluginAdminManagerStub) ReloadInstance(context.Context, string) error { return nil } diff --git a/backend/internal/app/pluginadmin/types.go b/backend/internal/app/pluginadmin/types.go index cf8f357f..0df24246 100644 --- a/backend/internal/app/pluginadmin/types.go +++ b/backend/internal/app/pluginadmin/types.go @@ -12,7 +12,7 @@ import ( type Manager interface { GetAllPluginMeta() []plugin.PluginMeta InstallFromBinary(context.Context, string, []byte) error - InstallFromGithub(context.Context, string) error + InstallFromGithub(context.Context, string, string) error Uninstall(context.Context, string) error ReloadDev(context.Context, string) error ReloadInstance(context.Context, string) error diff --git a/backend/internal/plugin/manager_install.go b/backend/internal/plugin/manager_install.go index d5772782..9668d429 100644 --- a/backend/internal/plugin/manager_install.go +++ b/backend/internal/plugin/manager_install.go @@ -7,6 +7,7 @@ import ( "io" "log/slog" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -53,19 +54,27 @@ func (m *Manager) InstallFromBinary(ctx context.Context, name string, binary []b realName = name } + targetDir := filepath.Join(m.pluginDir, realName) + binaryPath := filepath.Join(targetDir, realName) + previousBinary, previousErr := os.ReadFile(binaryPath) + m.stopPlugin(realName) - targetDir := filepath.Join(m.pluginDir, realName) if err := os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("创建插件目录失败: %w", err) } - binaryPath := filepath.Join(targetDir, realName) if err := os.WriteFile(binaryPath, binary, 0755); err != nil { return fmt.Errorf("写入插件二进制失败: %w", err) } canonicalName, err := m.startPlugin(ctx, realName, exec.Command(binaryPath), realName) if err != nil { + if previousErr == nil { + if restoreErr := m.restorePreviousBinary(ctx, realName, binaryPath, previousBinary); restoreErr != nil { + return fmt.Errorf("启动插件失败: %w;恢复旧版本也失败: %v", err, restoreErr) + } + slog.Warn("新插件启动失败,已恢复旧版本", "name", realName, "error", err) + } return fmt.Errorf("启动插件失败: %w", err) } @@ -73,6 +82,16 @@ func (m *Manager) InstallFromBinary(ctx context.Context, name string, binary []b return nil } +func (m *Manager) restorePreviousBinary(ctx context.Context, name, binaryPath string, previousBinary []byte) error { + if err := os.WriteFile(binaryPath, previousBinary, 0755); err != nil { + return fmt.Errorf("写回旧插件二进制失败: %w", err) + } + if _, err := m.startPlugin(ctx, name, exec.Command(binaryPath), name); err != nil { + return fmt.Errorf("重启旧插件失败: %w", err) + } + return nil +} + func (m *Manager) probePluginName(fallbackName string, binary []byte) (string, error) { tmpDir, err := os.MkdirTemp("", "airgate-probe-*") if err != nil { @@ -128,36 +147,16 @@ func (m *Manager) probePluginName(fallbackName string, binary []byte) (string, e } // InstallFromGithub 从 GitHub Release 下载并安装插件。 -func (m *Manager) InstallFromGithub(ctx context.Context, repo string) error { +// version 为空时安装 latest release;非空时按 release tag 安装,可用于回滚到旧版本。 +func (m *Manager) InstallFromGithub(ctx context.Context, repo, version string) error { owner, repoName, err := parseGithubRepo(repo) if err != nil { return err } - apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repoName) - req, _ := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) - req.Header.Set("Accept", "application/vnd.github.v3+json") - - resp, err := http.DefaultClient.Do(req) + release, err := fetchGithubReleaseForInstall(ctx, owner, repoName, version) if err != nil { - return fmt.Errorf("请求 GitHub API 失败: %w", err) - } - defer func() { - if err := resp.Body.Close(); err != nil { - slog.Warn("关闭 GitHub API 响应失败", "repo", repo, "error", err) - } - }() - - if resp.StatusCode == http.StatusNotFound { - return fmt.Errorf("仓库 %s/%s 不存在或没有 Release", owner, repoName) - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("GitHub API 返回状态码 %d", resp.StatusCode) - } - - var release githubRelease - if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { - return fmt.Errorf("解析 Release 数据失败: %w", err) + return err } targetOS := runtime.GOOS @@ -197,6 +196,84 @@ func (m *Manager) InstallFromGithub(ctx context.Context, repo string) error { return m.InstallFromBinary(ctx, repoName, binary) } +func fetchGithubReleaseForInstall(ctx context.Context, owner, repoName, version string) (githubRelease, error) { + var lastStatus int + for _, apiURL := range githubReleaseAPIURLs(owner, repoName, version) { + release, status, err := fetchGithubReleaseByURL(ctx, apiURL) + if err == nil { + return release, nil + } + lastStatus = status + if status != http.StatusNotFound { + return githubRelease{}, err + } + } + + if strings.TrimSpace(version) == "" { + return githubRelease{}, fmt.Errorf("仓库 %s/%s 不存在或没有 Release", owner, repoName) + } + if lastStatus == http.StatusNotFound { + return githubRelease{}, fmt.Errorf("仓库 %s/%s 不存在或没有 Release %s", owner, repoName, strings.TrimSpace(version)) + } + return githubRelease{}, fmt.Errorf("无法获取仓库 %s/%s 的 Release %s", owner, repoName, strings.TrimSpace(version)) +} + +func fetchGithubReleaseByURL(ctx context.Context, apiURL string) (githubRelease, int, error) { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) + req.Header.Set("Accept", "application/vnd.github.v3+json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return githubRelease{}, 0, fmt.Errorf("请求 GitHub API 失败: %w", err) + } + defer func() { + if err := resp.Body.Close(); err != nil { + slog.Warn("关闭 GitHub API 响应失败", "url", apiURL, "error", err) + } + }() + + if resp.StatusCode == http.StatusNotFound { + return githubRelease{}, resp.StatusCode, fmt.Errorf("GitHub Release 不存在") + } + if resp.StatusCode != http.StatusOK { + return githubRelease{}, resp.StatusCode, fmt.Errorf("GitHub API 返回状态码 %d", resp.StatusCode) + } + + var release githubRelease + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return githubRelease{}, resp.StatusCode, fmt.Errorf("解析 Release 数据失败: %w", err) + } + return release, resp.StatusCode, nil +} + +func githubReleaseAPIURLs(owner, repoName, version string) []string { + baseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repoName) + version = strings.TrimSpace(version) + if version == "" { + return []string{baseURL + "/latest"} + } + + tags := []string{version} + if strings.HasPrefix(version, "v") { + if trimmed := strings.TrimPrefix(version, "v"); trimmed != "" { + tags = append(tags, trimmed) + } + } else { + tags = append(tags, "v"+version) + } + + urls := make([]string, 0, len(tags)) + seen := make(map[string]struct{}, len(tags)) + for _, tag := range tags { + if _, ok := seen[tag]; ok { + continue + } + seen[tag] = struct{}{} + urls = append(urls, baseURL+"/tags/"+url.PathEscape(tag)) + } + return urls +} + type githubRelease struct { TagName string `json:"tag_name"` Assets []githubAsset `json:"assets"` diff --git a/backend/internal/plugin/manager_test.go b/backend/internal/plugin/manager_test.go index 667bdf33..8e9434b2 100644 --- a/backend/internal/plugin/manager_test.go +++ b/backend/internal/plugin/manager_test.go @@ -59,6 +59,52 @@ func TestParseGithubRepo(t *testing.T) { } } +func TestGithubReleaseAPIURLs(t *testing.T) { + tests := []struct { + name string + version string + want []string + }{ + { + name: "latest", + version: "", + want: []string{ + "https://api.github.com/repos/acme/airgate-plugin/releases/latest", + }, + }, + { + name: "plain version tries v-prefixed fallback", + version: "1.2.3", + want: []string{ + "https://api.github.com/repos/acme/airgate-plugin/releases/tags/1.2.3", + "https://api.github.com/repos/acme/airgate-plugin/releases/tags/v1.2.3", + }, + }, + { + name: "v-prefixed version tries plain fallback", + version: "v1.2.3", + want: []string{ + "https://api.github.com/repos/acme/airgate-plugin/releases/tags/v1.2.3", + "https://api.github.com/repos/acme/airgate-plugin/releases/tags/1.2.3", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := githubReleaseAPIURLs("acme", "airgate-plugin", tt.version) + if len(got) != len(tt.want) { + t.Fatalf("len(got) = %d, want %d: %#v", len(got), len(tt.want), got) + } + for i := range tt.want { + if got[i] != tt.want[i] { + t.Fatalf("got[%d] = %q, want %q", i, got[i], tt.want[i]) + } + } + }) + } +} + func TestGetModelsReturnsClone(t *testing.T) { mgr := &Manager{ modelCache: map[string][]sdk.ModelInfo{ diff --git a/backend/internal/server/dto/plugin.go b/backend/internal/server/dto/plugin.go index dc3c44ed..aa30974f 100644 --- a/backend/internal/server/dto/plugin.go +++ b/backend/internal/server/dto/plugin.go @@ -50,7 +50,8 @@ type FrontendPageResp struct { // InstallGithubReq 从 GitHub 安装插件请求 type InstallGithubReq struct { - Repo string `json:"repo" binding:"required"` // owner/repo 或完整 GitHub URL + Repo string `json:"repo" binding:"required"` // owner/repo 或完整 GitHub URL + Version string `json:"version,omitempty"` // 可选 release tag,留空安装 latest } // PluginOAuthStartResp 插件 OAuth 授权开始响应 diff --git a/backend/internal/server/handler/plugin_handler_routes.go b/backend/internal/server/handler/plugin_handler_routes.go index 7c67867e..d48a6438 100644 --- a/backend/internal/server/handler/plugin_handler_routes.go +++ b/backend/internal/server/handler/plugin_handler_routes.go @@ -130,7 +130,7 @@ func (h *PluginHandler) InstallFromGithub(c *gin.Context) { return } - if err := h.service.InstallFromGithub(c.Request.Context(), req.Repo); err != nil { + if err := h.service.InstallFromGithub(c.Request.Context(), req.Repo, req.Version); err != nil { response.InternalError(c, "从 GitHub 安装失败: "+err.Error()) return } diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json index 90e1e04f..69550ae3 100644 --- a/web/src/i18n/en.json +++ b/web/src/i18n/en.json @@ -739,6 +739,10 @@ "plugin_name_hint": "Leave empty to use filename", "github_repo": "Repository", "github_repo_placeholder": "owner/repo", + "github_version": "Release Version", + "github_version_placeholder": "Leave empty for latest, e.g. v1.2.3", + "github_version_hint": "Use an older release tag to roll back", + "version_install": "Install Specific Version", "github_hint": "Download pre-built binary from GitHub Release for the current platform", "upload_success": "Plugin uploaded and installed", "github_success": "Plugin installed from GitHub", diff --git a/web/src/i18n/zh.json b/web/src/i18n/zh.json index b7113e52..84ac8642 100644 --- a/web/src/i18n/zh.json +++ b/web/src/i18n/zh.json @@ -742,6 +742,10 @@ "plugin_name_hint": "留空则使用文件名", "github_repo": "仓库地址", "github_repo_placeholder": "owner/repo", + "github_version": "Release 版本", + "github_version_placeholder": "留空安装最新版,例如 v1.2.3", + "github_version_hint": "填写旧版本 tag 可回滚到指定版本", + "version_install": "安装指定版本", "github_hint": "从 GitHub Release 下载适配当前系统的预编译二进制文件", "upload_success": "插件上传安装成功", "github_success": "插件从 GitHub 安装成功", diff --git a/web/src/pages/admin/PluginsPage.tsx b/web/src/pages/admin/PluginsPage.tsx index 8a1f4d0b..c0a13b4f 100644 --- a/web/src/pages/admin/PluginsPage.tsx +++ b/web/src/pages/admin/PluginsPage.tsx @@ -11,11 +11,16 @@ import { AlertDialog, Button, Card, Checkbox, Chip, Description, EmptyState, Inp import { DialogTriggerShim } from '../../shared/components/DialogTriggerShim'; import { Trash2, Download, Loader2, RefreshCw, - Package, User, Tag, Plus, Upload, Github, Settings, Store, + Package, User, Tag, Plus, Upload, Github, Settings, Store, History, } from 'lucide-react'; import { CommonTable } from '../../shared/components/CommonTable'; import type { PluginResp, MarketplacePluginResp } from '../../shared/types'; +type InstallPrefill = { + repo: string; + version?: string; +} | null; + // 插件类型 Badge 颜色 const typeVariant: Record = { gateway: 'accent', @@ -31,6 +36,7 @@ export default function PluginsPage() { const [activeTab, setActiveTab] = useState<'installed' | 'marketplace'>('installed'); const [uninstallTarget, setUninstallTarget] = useState(null); const [installOpen, setInstallOpen] = useState(false); + const [installPrefill, setInstallPrefill] = useState(null); const [configTarget, setConfigTarget] = useState(null); // 已安装插件列表 @@ -50,7 +56,7 @@ export default function PluginsPage() { const [installingRepo, setInstallingRepo] = useState(null); const [isUpdating, setIsUpdating] = useState(false); const marketInstallMutation = useMutation({ - mutationFn: (repo: string) => pluginsApi.installGithub(repo), + mutationFn: ({ repo, version }: { repo: string; version?: string }) => pluginsApi.installGithub(repo, version), onSuccess: () => { toast('success', t(isUpdating ? 'plugins.update_success' : 'plugins.github_success')); // 插件前端模块需要整页重载才能生效 @@ -63,10 +69,15 @@ export default function PluginsPage() { }, }); - function handleMarketInstall(repo: string, update = false) { + function handleMarketInstall(repo: string, update = false, version?: string) { setInstallingRepo(repo); setIsUpdating(update); - marketInstallMutation.mutate(repo); + marketInstallMutation.mutate({ repo, version }); + } + + function openVersionInstall(repo: string, version = '') { + setInstallPrefill({ repo, version }); + setInstallOpen(true); } // 强制从 GitHub 同步市场列表(点击右上角刷新按钮时触发) @@ -152,7 +163,10 @@ export default function PluginsPage() { ) : ( - {t('plugins.already_installed')} +
+ {t('plugins.already_installed')} + +
) ) : ( - +
+ + +
)} diff --git a/web/src/shared/api/plugins.ts b/web/src/shared/api/plugins.ts index 9b54717a..35bb3746 100644 --- a/web/src/shared/api/plugins.ts +++ b/web/src/shared/api/plugins.ts @@ -22,8 +22,8 @@ export const pluginsApi = { return upload('/api/v1/admin/plugins/upload', fd); }, // 从 GitHub Release 安装 - installGithub: (repo: string) => - post('/api/v1/admin/plugins/install-github', { repo }), + installGithub: (repo: string, version?: string) => + post('/api/v1/admin/plugins/install-github', { repo, version }), // 通用插件 RPC 调用,action 由插件自行定义 rpc: (name: string, action: string, body?: unknown) => post(`/api/v1/admin/plugins/${name}/rpc/${action}`, body), From a6118a5e204f38c3a6ecae616b4bcb5729c5a979 Mon Sep 17 00:00:00 2001 From: quantal Date: Wed, 27 May 2026 17:11:45 +0800 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E6=9A=82=E6=97=B6=E4=B8=8D=E5=8F=AF=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/account/service.go | 5 + backend/internal/app/account/service_test.go | 8 ++ backend/internal/plugin/forwarder.go | 2 + backend/internal/plugin/host_service.go | 4 + backend/internal/plugin/outcome.go | 2 + backend/internal/scheduler/admin.go | 15 ++- backend/internal/scheduler/state.go | 121 +++++++++++++++++- backend/internal/scheduler/state_test.go | 127 +++++++++++++++++++ 8 files changed, 277 insertions(+), 7 deletions(-) create mode 100644 backend/internal/scheduler/state_test.go diff --git a/backend/internal/app/account/service.go b/backend/internal/app/account/service.go index df016d30..7f9cd5f3 100644 --- a/backend/internal/app/account/service.go +++ b/backend/internal/app/account/service.go @@ -523,6 +523,11 @@ func connectivityTestErrorMessage(outcome sdk.ForwardOutcome) string { return "上游账号不可用: " + reason } return "上游账号不可用,请检查凭证或账号状态" + case sdk.OutcomeAccountUnavailable: + if reason != "" { + return "上游账号403暂不可用: " + reason + } + return "上游账号403暂不可用,请稍后重试" case sdk.OutcomeStreamAborted: return "上游响应流中断,请稍后重试或查看上游日志" case sdk.OutcomeUpstreamTransient: diff --git a/backend/internal/app/account/service_test.go b/backend/internal/app/account/service_test.go index 8a0eb395..c971a7ae 100644 --- a/backend/internal/app/account/service_test.go +++ b/backend/internal/app/account/service_test.go @@ -818,6 +818,14 @@ func TestConnectivityTestErrorMessage(t *testing.T) { }, want: "上游账号不可用: HTTP 401: Your authentication token has been invalidated", }, + { + name: "账号暂时不可用使用统一提示", + outcome: sdk.ForwardOutcome{ + Kind: sdk.OutcomeAccountUnavailable, + Reason: "HTTP 403: 访问被拒绝,账号可能已被禁用或无权限", + }, + want: "上游账号403暂不可用: HTTP 403: 访问被拒绝,账号可能已被禁用或无权限", + }, } for _, c := range cases { diff --git a/backend/internal/plugin/forwarder.go b/backend/internal/plugin/forwarder.go index 8c552a66..6558780a 100644 --- a/backend/internal/plugin/forwarder.go +++ b/backend/internal/plugin/forwarder.go @@ -368,6 +368,8 @@ func (s *allRoutesFailureSummary) recordExecution(execution forwardExecution) { s.recordRetryAfter(execution.outcome.RetryAfter) case sdk.OutcomeAccountDead: s.accountDeadSeen = true + case sdk.OutcomeAccountUnavailable: + s.accountUnavailable = true case sdk.OutcomeUpstreamTransient: if isTimeoutFailure(execution) { s.upstreamTimeoutSeen = true diff --git a/backend/internal/plugin/host_service.go b/backend/internal/plugin/host_service.go index 4be176bb..931738d6 100644 --- a/backend/internal/plugin/host_service.go +++ b/backend/internal/plugin/host_service.go @@ -629,6 +629,10 @@ func (h *HostService) probeForward(ctx context.Context, req hostProbeForwardRequ resp["success"] = false resp["error_kind"] = "account_error" resp["error_msg"] = truncateProbeErr(outcome.Reason) + case sdk.OutcomeAccountUnavailable: + resp["success"] = false + resp["error_kind"] = "account_unavailable" + resp["error_msg"] = truncateProbeErr(outcome.Reason) case sdk.OutcomeUpstreamTransient, sdk.OutcomeStreamAborted: resp["success"] = false resp["error_kind"] = "upstream_5xx" diff --git a/backend/internal/plugin/outcome.go b/backend/internal/plugin/outcome.go index 58b4f792..becbb15c 100644 --- a/backend/internal/plugin/outcome.go +++ b/backend/internal/plugin/outcome.go @@ -192,6 +192,8 @@ func sanitizedMessage(kind sdk.OutcomeKind) string { return "上游账号当前被限流,请稍后重试" case sdk.OutcomeAccountDead: return "上游账号不可用,请联系管理员" + case sdk.OutcomeAccountUnavailable: + return "上游账号403暂不可用,请稍后重试" case sdk.OutcomeStreamAborted: return "响应流中断" case sdk.OutcomeUpstreamTransient: diff --git a/backend/internal/scheduler/admin.go b/backend/internal/scheduler/admin.go index fdc9faf3..64b73f4a 100644 --- a/backend/internal/scheduler/admin.go +++ b/backend/internal/scheduler/admin.go @@ -14,11 +14,20 @@ import ( func (s *Scheduler) ManualRecover(ctx context.Context, accountID int) error { dbCtx, cancel := context.WithTimeout(ctx, dbTimeout) defer cancel() - err := s.db.Account.UpdateOneID(accountID). + + upd := s.db.Account.UpdateOneID(accountID). SetState(account.StateActive). ClearStateUntil(). - SetErrorMsg(""). - Exec(dbCtx) + SetErrorMsg("") + if existing, getErr := s.db.Account.Get(dbCtx, accountID); getErr == nil { + if extraInt(existing.Extra, accountUnavailableCountExtraKey) > 0 { + extra := cloneExtra(existing.Extra) + delete(extra, accountUnavailableCountExtraKey) + upd = upd.SetExtra(extra) + } + } + + err := upd.Exec(dbCtx) if err == nil { s.routeCache.InvalidateAll() } diff --git a/backend/internal/scheduler/state.go b/backend/internal/scheduler/state.go index 357c2c41..1ebf47e2 100644 --- a/backend/internal/scheduler/state.go +++ b/backend/internal/scheduler/state.go @@ -26,6 +26,10 @@ const ( degradedDefault = 60 * time.Second // degradedMax 池账号最长降级窗口。 degradedMax = 10 * time.Minute + // accountUnavailableThreshold 账号短暂 403 连续达到该次数后升级为 disabled。 + accountUnavailableThreshold = 3 + + accountUnavailableCountExtraKey = "_airgate_account_unavailable_count" ) // Judgment forwarder 对一次调用的判决,交给状态机做状态转移。 @@ -78,6 +82,7 @@ func (sm *StateMachine) notifyCritical() { // Success → state=active,清 state_until,last_used_at=now // AccountRateLimited → state=rate_limited,state_until=now+RetryAfter // AccountDead → state=disabled(凭证失效,需人工介入) +// AccountUnavailable → state=degraded,累计 3 次后升级 disabled // UpstreamTransient → 非池:**不动状态**(上游抖动不扣账号分,靠 failover 切走就行);池:state=degraded // ClientError / StreamAborted / Unknown → 不改状态(账号无辜) func (sm *StateMachine) Apply(ctx context.Context, accountID int, j Judgment) { @@ -120,6 +125,9 @@ func (sm *StateMachine) Apply(ctx context.Context, accountID int, j Judgment) { } sm.transition(ctx, accountID, account.StateDisabled, nil, j.Reason) + case sdk.OutcomeAccountUnavailable: + sm.applyAccountUnavailable(ctx, accountID, j.Reason) + case sdk.OutcomeUpstreamTransient: // 按定义,UpstreamTransient 是"上游侧瞬时故障"(SSE 提前断流、网络抖动、上游 5xx 等), // 账号本身没问题——不动 state,让 failover 切到下一账号就够了。 @@ -145,6 +153,74 @@ func (sm *StateMachine) applyDegraded(ctx context.Context, accountID int, reason sm.transition(ctx, accountID, account.StateDegraded, &until, reason) } +func (sm *StateMachine) applyAccountUnavailable(ctx context.Context, accountID int, reason string) { + dbCtx, cancel := context.WithTimeout(context.Background(), dbTimeout) + defer cancel() + + existing, err := sm.db.Account.Get(dbCtx, accountID) + if err != nil { + slog.Warn("scheduler_account_unavailable_load_failed", + sdk.LogFieldAccountID, accountID, sdk.LogFieldError, err) + return + } + + extra := cloneExtra(existing.Extra) + now := time.Now() + if existing.State == account.StateDegraded && existing.StateUntil != nil && existing.StateUntil.After(now) && extraInt(extra, accountUnavailableCountExtraKey) > 0 { + slog.Warn("scheduler_account_unavailable_degraded_skip_count", + sdk.LogFieldAccountID, accountID, + "count", extraInt(extra, accountUnavailableCountExtraKey), + "until", existing.StateUntil, + sdk.LogFieldReason, reason, + ) + return + } + + count := extraInt(extra, accountUnavailableCountExtraKey) + 1 + extra[accountUnavailableCountExtraKey] = count + + if count >= accountUnavailableThreshold { + delete(extra, accountUnavailableCountExtraKey) + err = sm.db.Account.UpdateOneID(accountID). + SetState(account.StateDisabled). + ClearStateUntil(). + SetErrorMsg(truncateReason(reason)). + SetExtra(extra). + Exec(dbCtx) + if err != nil { + slog.Error("scheduler_account_unavailable_disable_failed", + sdk.LogFieldAccountID, accountID, sdk.LogFieldError, err) + return + } + slog.Warn("scheduler_account_unavailable_escalated", + sdk.LogFieldAccountID, accountID, + "count", count, + sdk.LogFieldReason, reason, + ) + sm.notifyCritical() + return + } + + until := now.Add(degradedDefault) + err = sm.db.Account.UpdateOneID(accountID). + SetState(account.StateDegraded). + SetStateUntil(until). + SetErrorMsg(truncateReason(reason)). + SetExtra(extra). + Exec(dbCtx) + if err != nil { + slog.Error("scheduler_account_unavailable_degrade_failed", + sdk.LogFieldAccountID, accountID, sdk.LogFieldError, err) + return + } + slog.Warn("scheduler_account_unavailable_degraded", + sdk.LogFieldAccountID, accountID, + "count", count, + "until", until, + sdk.LogFieldReason, reason, + ) +} + // transitionActive 成功时回到 active:清 state_until、清 reason、清失败计数、更新 last_used_at。 // // disabled 状态受保护:只有管理员操作(ManualRecover / ToggleScheduling)才能解除, @@ -155,7 +231,8 @@ func (sm *StateMachine) transitionActive(ctx context.Context, accountID int) { defer cancel() prevState := account.StateActive - if existing, err := sm.db.Account.Get(dbCtx, accountID); err == nil { + existing, getErr := sm.db.Account.Get(dbCtx, accountID) + if getErr == nil { prevState = existing.State } @@ -166,12 +243,20 @@ func (sm *StateMachine) transitionActive(ctx context.Context, accountID int) { return } - err := sm.db.Account.UpdateOneID(accountID). + upd := sm.db.Account.UpdateOneID(accountID). SetState(account.StateActive). ClearStateUntil(). SetErrorMsg(""). - SetLastUsedAt(now). - Exec(dbCtx) + SetLastUsedAt(now) + if getErr == nil { + if extraInt(existing.Extra, accountUnavailableCountExtraKey) > 0 { + extra := cloneExtra(existing.Extra) + delete(extra, accountUnavailableCountExtraKey) + upd = upd.SetExtra(extra) + } + } + + err := upd.Exec(dbCtx) if err != nil { slog.Warn("scheduler_state_active_failed", sdk.LogFieldAccountID, accountID, sdk.LogFieldError, err) @@ -252,3 +337,31 @@ func truncateReason(s string) string { } return s } + +func cloneExtra(input map[string]interface{}) map[string]interface{} { + if len(input) == 0 { + return map[string]interface{}{} + } + out := make(map[string]interface{}, len(input)) + for k, v := range input { + out[k] = v + } + return out +} + +func extraInt(extra map[string]interface{}, key string) int { + switch v := extra[key].(type) { + case int: + return v + case int32: + return int(v) + case int64: + return int(v) + case float64: + return int(v) + case float32: + return int(v) + default: + return 0 + } +} diff --git a/backend/internal/scheduler/state_test.go b/backend/internal/scheduler/state_test.go new file mode 100644 index 00000000..8450c3db --- /dev/null +++ b/backend/internal/scheduler/state_test.go @@ -0,0 +1,127 @@ +package scheduler + +import ( + "context" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" + + "github.com/DouDOU-start/airgate-core/ent" + "github.com/DouDOU-start/airgate-core/ent/account" + "github.com/DouDOU-start/airgate-core/ent/enttest" + "github.com/DouDOU-start/airgate-core/ent/migrate" + sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +) + +func TestStateMachineAccountUnavailableEscalatesAfterThreshold(t *testing.T) { + ctx := context.Background() + db := openStateMachineTestDB(t, "scheduler_account_unavailable_threshold") + sm := NewStateMachine(db, nil, nil) + criticalTransitions := 0 + sm.onCriticalTransition = func() { criticalTransitions++ } + + acc := db.Account.Create(). + SetName("temporary 403"). + SetPlatform("openai"). + SetType("apikey"). + SetCredentials(map[string]string{}). + SaveX(ctx) + + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeAccountUnavailable, Reason: "HTTP 403"}) + fresh := db.Account.GetX(ctx, acc.ID) + if fresh.State != account.StateDegraded { + t.Fatalf("state after first unavailable = %s, want degraded", fresh.State) + } + if fresh.StateUntil == nil { + t.Fatalf("state_until should be set after first unavailable") + } + if got := extraInt(fresh.Extra, accountUnavailableCountExtraKey); got != 1 { + t.Fatalf("unavailable count after first unavailable = %d, want 1", got) + } + + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeAccountUnavailable, Reason: "HTTP 403"}) + fresh = db.Account.GetX(ctx, acc.ID) + if got := extraInt(fresh.Extra, accountUnavailableCountExtraKey); got != 1 { + t.Fatalf("unavailable count during degraded window = %d, want 1", got) + } + + expireAccountDegradedWindow(ctx, db, acc.ID) + + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeAccountUnavailable, Reason: "HTTP 403"}) + fresh = db.Account.GetX(ctx, acc.ID) + if fresh.State != account.StateDegraded { + t.Fatalf("state after second unavailable = %s, want degraded", fresh.State) + } + if got := extraInt(fresh.Extra, accountUnavailableCountExtraKey); got != 2 { + t.Fatalf("unavailable count after second unavailable = %d, want 2", got) + } + + expireAccountDegradedWindow(ctx, db, acc.ID) + + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeAccountUnavailable, Reason: "HTTP 403"}) + fresh = db.Account.GetX(ctx, acc.ID) + if fresh.State != account.StateDisabled { + t.Fatalf("state after third unavailable = %s, want disabled", fresh.State) + } + if fresh.StateUntil != nil { + t.Fatalf("state_until should be cleared after escalation") + } + if got := extraInt(fresh.Extra, accountUnavailableCountExtraKey); got != 0 { + t.Fatalf("unavailable count after escalation = %d, want cleared", got) + } + if criticalTransitions != 1 { + t.Fatalf("critical transitions = %d, want 1", criticalTransitions) + } +} + +func TestStateMachineSuccessClearsAccountUnavailableCount(t *testing.T) { + ctx := context.Background() + db := openStateMachineTestDB(t, "scheduler_account_unavailable_success") + sm := NewStateMachine(db, nil, nil) + + acc := db.Account.Create(). + SetName("temporary 403"). + SetPlatform("openai"). + SetType("apikey"). + SetCredentials(map[string]string{}). + SetExtra(map[string]interface{}{"keep": "value"}). + SaveX(ctx) + + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeAccountUnavailable, Reason: "HTTP 403"}) + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeSuccess}) + + fresh := db.Account.GetX(ctx, acc.ID) + if fresh.State != account.StateActive { + t.Fatalf("state after success = %s, want active", fresh.State) + } + if fresh.StateUntil != nil { + t.Fatalf("state_until should be cleared after success") + } + if fresh.ErrorMsg != "" { + t.Fatalf("error_msg after success = %q, want empty", fresh.ErrorMsg) + } + if got := extraInt(fresh.Extra, accountUnavailableCountExtraKey); got != 0 { + t.Fatalf("unavailable count after success = %d, want cleared", got) + } + if fresh.Extra["keep"] != "value" { + t.Fatalf("unrelated extra value was not preserved: %+v", fresh.Extra) + } +} + +func openStateMachineTestDB(t *testing.T, name string) *ent.Client { + t.Helper() + db := enttest.Open(t, "sqlite3", "file:"+name+"?mode=memory&cache=shared&_fk=1", enttest.WithMigrateOptions(migrate.WithGlobalUniqueID(false))) + t.Cleanup(func() { + if err := db.Close(); err != nil { + t.Fatalf("close db: %v", err) + } + }) + return db +} + +func expireAccountDegradedWindow(ctx context.Context, db *ent.Client, accountID int) { + db.Account.UpdateOneID(accountID). + SetStateUntil(time.Now().Add(-time.Second)). + ExecX(ctx) +} From 116883df52627d6e78740d55e9b9ed7d3741bf7d Mon Sep 17 00:00:00 2001 From: quantal Date: Wed, 27 May 2026 17:26:01 +0800 Subject: [PATCH 04/19] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9=E9=9D=9E=E6=B1=A0=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=9A=84=E8=B7=9F=E8=B8=AA=E4=B8=8E=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/scheduler/state.go | 16 ++- backend/internal/scheduler/state_test.go | 130 ++++++++++++++++++++++- 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/backend/internal/scheduler/state.go b/backend/internal/scheduler/state.go index 1ebf47e2..e8583dfc 100644 --- a/backend/internal/scheduler/state.go +++ b/backend/internal/scheduler/state.go @@ -82,7 +82,7 @@ func (sm *StateMachine) notifyCritical() { // Success → state=active,清 state_until,last_used_at=now // AccountRateLimited → state=rate_limited,state_until=now+RetryAfter // AccountDead → state=disabled(凭证失效,需人工介入) -// AccountUnavailable → state=degraded,累计 3 次后升级 disabled +// AccountUnavailable → 非池账号 state=degraded,累计 3 次后升级 disabled // UpstreamTransient → 非池:**不动状态**(上游抖动不扣账号分,靠 failover 切走就行);池:state=degraded // ClientError / StreamAborted / Unknown → 不改状态(账号无辜) func (sm *StateMachine) Apply(ctx context.Context, accountID int, j Judgment) { @@ -164,6 +164,16 @@ func (sm *StateMachine) applyAccountUnavailable(ctx context.Context, accountID i return } + if !shouldTrackAccountUnavailable(existing) { + slog.Warn("scheduler_account_unavailable_ignored", + sdk.LogFieldAccountID, accountID, + "account_type", existing.Type, + "is_pool", existing.UpstreamIsPool, + sdk.LogFieldReason, reason, + ) + return + } + extra := cloneExtra(existing.Extra) now := time.Now() if existing.State == account.StateDegraded && existing.StateUntil != nil && existing.StateUntil.After(now) && extraInt(extra, accountUnavailableCountExtraKey) > 0 { @@ -221,6 +231,10 @@ func (sm *StateMachine) applyAccountUnavailable(ctx context.Context, accountID i ) } +func shouldTrackAccountUnavailable(acc *ent.Account) bool { + return acc != nil && !acc.UpstreamIsPool +} + // transitionActive 成功时回到 active:清 state_until、清 reason、清失败计数、更新 last_used_at。 // // disabled 状态受保护:只有管理员操作(ManualRecover / ToggleScheduling)才能解除, diff --git a/backend/internal/scheduler/state_test.go b/backend/internal/scheduler/state_test.go index 8450c3db..8565f626 100644 --- a/backend/internal/scheduler/state_test.go +++ b/backend/internal/scheduler/state_test.go @@ -24,7 +24,7 @@ func TestStateMachineAccountUnavailableEscalatesAfterThreshold(t *testing.T) { acc := db.Account.Create(). SetName("temporary 403"). SetPlatform("openai"). - SetType("apikey"). + SetType("oauth"). SetCredentials(map[string]string{}). SaveX(ctx) @@ -83,7 +83,7 @@ func TestStateMachineSuccessClearsAccountUnavailableCount(t *testing.T) { acc := db.Account.Create(). SetName("temporary 403"). SetPlatform("openai"). - SetType("apikey"). + SetType("oauth"). SetCredentials(map[string]string{}). SetExtra(map[string]interface{}{"keep": "value"}). SaveX(ctx) @@ -109,6 +109,132 @@ func TestStateMachineSuccessClearsAccountUnavailableCount(t *testing.T) { } } +func TestShouldTrackAccountUnavailableOnlyNonPool(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + acc *ent.Account + want bool + }{ + { + name: "oauth account", + acc: &ent.Account{Type: "oauth"}, + want: true, + }, + { + name: "oauth pool account", + acc: &ent.Account{Type: "oauth", UpstreamIsPool: true}, + want: false, + }, + { + name: "api key account", + acc: &ent.Account{Type: "apikey"}, + want: true, + }, + { + name: "api key pool account", + acc: &ent.Account{Type: "apikey", UpstreamIsPool: true}, + want: false, + }, + { + name: "nil account", + acc: nil, + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := shouldTrackAccountUnavailable(tt.acc); got != tt.want { + t.Fatalf("shouldTrackAccountUnavailable() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStateMachineAccountUnavailableTracksNormalAPIKey(t *testing.T) { + ctx := context.Background() + db := openStateMachineTestDB(t, "scheduler_account_unavailable_api_key") + sm := NewStateMachine(db, nil, nil) + + acc := db.Account.Create(). + SetName("api key"). + SetPlatform("openai"). + SetType("apikey"). + SetCredentials(map[string]string{}). + SaveX(ctx) + + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeAccountUnavailable, Reason: "HTTP 403"}) + + fresh := db.Account.GetX(ctx, acc.ID) + if fresh.State != account.StateDegraded { + t.Fatalf("state after api key unavailable = %s, want degraded", fresh.State) + } + if fresh.StateUntil == nil { + t.Fatalf("state_until should be set for normal api key account") + } + if got := extraInt(fresh.Extra, accountUnavailableCountExtraKey); got != 1 { + t.Fatalf("unavailable count for normal api key account = %d, want 1", got) + } +} + +func TestStateMachineAccountUnavailableIgnoredForAPIPool(t *testing.T) { + ctx := context.Background() + db := openStateMachineTestDB(t, "scheduler_account_unavailable_api_pool") + sm := NewStateMachine(db, nil, nil) + + acc := db.Account.Create(). + SetName("api pool"). + SetPlatform("openai"). + SetType("apikey"). + SetUpstreamIsPool(true). + SetCredentials(map[string]string{}). + SaveX(ctx) + + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeAccountUnavailable, Reason: "HTTP 403"}) + + fresh := db.Account.GetX(ctx, acc.ID) + if fresh.State != account.StateActive { + t.Fatalf("state after api pool unavailable = %s, want active", fresh.State) + } + if fresh.StateUntil != nil { + t.Fatalf("state_until should remain empty for api pool account") + } + if got := extraInt(fresh.Extra, accountUnavailableCountExtraKey); got != 0 { + t.Fatalf("unavailable count for api pool account = %d, want 0", got) + } +} + +func TestStateMachineAccountUnavailableIgnoredForOAuthPool(t *testing.T) { + ctx := context.Background() + db := openStateMachineTestDB(t, "scheduler_account_unavailable_oauth_pool") + sm := NewStateMachine(db, nil, nil) + + acc := db.Account.Create(). + SetName("oauth pool"). + SetPlatform("openai"). + SetType("oauth"). + SetUpstreamIsPool(true). + SetCredentials(map[string]string{}). + SaveX(ctx) + + sm.Apply(ctx, acc.ID, Judgment{Kind: sdk.OutcomeAccountUnavailable, Reason: "HTTP 403"}) + + fresh := db.Account.GetX(ctx, acc.ID) + if fresh.State != account.StateActive { + t.Fatalf("state after oauth pool unavailable = %s, want active", fresh.State) + } + if fresh.StateUntil != nil { + t.Fatalf("state_until should remain empty for oauth pool account") + } + if got := extraInt(fresh.Extra, accountUnavailableCountExtraKey); got != 0 { + t.Fatalf("unavailable count for oauth pool account = %d, want 0", got) + } +} + func openStateMachineTestDB(t *testing.T, name string) *ent.Client { t.Helper() db := enttest.Open(t, "sqlite3", "file:"+name+"?mode=memory&cache=shared&_fk=1", enttest.WithMigrateOptions(migrate.WithGlobalUniqueID(false))) From 362c01820bc3bea94b036171f5e1cbf6c4e18df4 Mon Sep 17 00:00:00 2001 From: quantal Date: Wed, 27 May 2026 18:17:17 +0800 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E8=AE=B0=E5=BD=95=E6=9F=A5=E8=AF=A2=E5=BC=95=E8=B5=B7?= =?UTF-8?q?=E7=9A=84=E6=80=A7=E8=83=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/infra/store/usage_store.go | 4 ++-- backend/internal/infra/store/usage_store_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/internal/infra/store/usage_store.go b/backend/internal/infra/store/usage_store.go index b14eb49e..303663c4 100644 --- a/backend/internal/infra/store/usage_store.go +++ b/backend/internal/infra/store/usage_store.go @@ -44,7 +44,7 @@ func (s *UsageStore) ListUser(ctx context.Context, userID int64, filter appusage WithGroup(). Offset((filter.Page-1)*filter.PageSize). Limit(filter.PageSize). - Order(ent.Desc(entusagelog.FieldCreatedAt), ent.Desc(entusagelog.FieldID)). + Order(ent.Desc(entusagelog.FieldID)). All(ctx) if err != nil { return nil, 0, err @@ -77,7 +77,7 @@ func (s *UsageStore) ListAdmin(ctx context.Context, filter appusage.ListFilter) WithGroup(). Offset((filter.Page-1)*filter.PageSize). Limit(filter.PageSize). - Order(ent.Desc(entusagelog.FieldCreatedAt), ent.Desc(entusagelog.FieldID)). + Order(ent.Desc(entusagelog.FieldID)). All(ctx) if err != nil { return nil, 0, err diff --git a/backend/internal/infra/store/usage_store_test.go b/backend/internal/infra/store/usage_store_test.go index 50a691b1..071b12d4 100644 --- a/backend/internal/infra/store/usage_store_test.go +++ b/backend/internal/infra/store/usage_store_test.go @@ -8,7 +8,7 @@ import ( appusage "github.com/DouDOU-start/airgate-core/internal/app/usage" ) -func TestUsageStoreListPaginationIsStableForIdenticalCreatedAt(t *testing.T) { +func TestUsageStoreListPaginationUsesStableIDOrder(t *testing.T) { db := enttestOpen(t) defer func() { if err := db.Close(); err != nil { From 24eca9e2698e6f2a009a8360f46320d8c6414fe5 Mon Sep 17 00:00:00 2001 From: quantal Date: Wed, 27 May 2026 21:29:35 +0800 Subject: [PATCH 06/19] =?UTF-8?q?Refactor=20usage=20tracking:=20Usage?= =?UTF-8?q?=E8=A1=A8=E5=AD=97=E6=AE=B5=E9=87=8D=E6=9E=84=EF=BC=8C=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/usage/types.go | 9 +- backend/internal/billing/recorder.go | 48 +----- backend/internal/infra/store/usage_store.go | 55 +++++- backend/internal/plugin/host_service.go | 9 +- backend/internal/plugin/image_pricing_test.go | 16 +- backend/internal/plugin/outcome.go | 15 +- backend/internal/plugin/usage_adapter.go | 162 +++++++----------- backend/internal/server/dto/usage.go | 145 ++++++++-------- .../server/handler/usage_handler_mapper.go | 5 - web/src/shared/columns/usageColumns.tsx | 125 ++++++-------- web/src/shared/types/index.ts | 34 ---- 11 files changed, 250 insertions(+), 373 deletions(-) diff --git a/backend/internal/app/usage/types.go b/backend/internal/app/usage/types.go index 8b5bfb42..c252fec8 100644 --- a/backend/internal/app/usage/types.go +++ b/backend/internal/app/usage/types.go @@ -1,10 +1,6 @@ package usage -import ( - "context" - - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" -) +import "context" // ListFilter 使用记录列表筛选。 type ListFilter struct { @@ -92,9 +88,6 @@ type LogRecord struct { IPAddress string Endpoint string ReasoningEffort string - UsageAttributes []sdk.UsageAttribute - UsageMetrics []sdk.UsageMetric - UsageCostDetails []sdk.UsageCostDetail UsageMetadata map[string]string CreatedAt string } diff --git a/backend/internal/billing/recorder.go b/backend/internal/billing/recorder.go index e22c111a..26455caa 100644 --- a/backend/internal/billing/recorder.go +++ b/backend/internal/billing/recorder.go @@ -8,7 +8,6 @@ import ( "time" "github.com/DouDOU-start/airgate-core/ent" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) const ( @@ -58,9 +57,6 @@ type UsageRecord struct { IPAddress string Endpoint string ReasoningEffort string - UsageAttributes []sdk.UsageAttribute - UsageMetrics []sdk.UsageMetric - UsageCostDetails []sdk.UsageCostDetail UsageMetadata map[string]string } @@ -266,10 +262,6 @@ func usageLogCreate(tx *ent.Tx, rec UsageRecord) *ent.UsageLogCreate { SetIPAddress(rec.IPAddress). SetEndpoint(rec.Endpoint). SetReasoningEffort(rec.ReasoningEffort). - SetUsageAttributes(rec.UsageAttributes). - SetUsageMetrics(rec.UsageMetrics). - SetUsageCostDetails(enrichUsageCostDetails(rec)). - SetUsageMetadata(rec.UsageMetadata). SetUserIDSnapshot(rec.UserID). SetUserID(rec.UserID). SetAccountID(rec.AccountID). @@ -277,44 +269,10 @@ func usageLogCreate(tx *ent.Tx, rec UsageRecord) *ent.UsageLogCreate { if rec.APIKeyID > 0 { b.SetAPIKeyID(rec.APIKeyID) } - return b -} - -func enrichUsageCostDetails(rec UsageRecord) []sdk.UsageCostDetail { - if len(rec.UsageCostDetails) == 0 { - return rec.UsageCostDetails + if len(rec.UsageMetadata) > 0 { + b.SetUsageMetadata(rec.UsageMetadata) } - - items := make([]sdk.UsageCostDetail, len(rec.UsageCostDetails)) - copy(items, rec.UsageCostDetails) - - var accountCostSum float64 - for _, item := range items { - if item.AccountCost > 0 { - accountCostSum += item.AccountCost - } - } - - for i := range items { - accountCost := items[i].AccountCost - if accountCost <= 0 { - if rec.RateMultiplier > 0 { - items[i].BillingMultiplier = rec.RateMultiplier - } - continue - } - if accountCostSum > 0 && rec.ActualCost > 0 { - items[i].UserCost = rec.ActualCost * accountCost / accountCostSum - items[i].BillingMultiplier = items[i].UserCost / accountCost - continue - } - if rec.RateMultiplier > 0 { - items[i].BillingMultiplier = rec.RateMultiplier - items[i].UserCost = accountCost * rec.RateMultiplier - } - } - - return items + return b } func applyUsageCharges(ctx context.Context, tx *ent.Tx, batch []UsageRecord) error { diff --git a/backend/internal/infra/store/usage_store.go b/backend/internal/infra/store/usage_store.go index 303663c4..8c6268dc 100644 --- a/backend/internal/infra/store/usage_store.go +++ b/backend/internal/infra/store/usage_store.go @@ -21,6 +21,52 @@ type UsageStore struct { db *ent.Client } +var usageLogListFields = []string{ + entusagelog.FieldID, + entusagelog.FieldPlatform, + entusagelog.FieldModel, + entusagelog.FieldInputTokens, + entusagelog.FieldOutputTokens, + entusagelog.FieldCachedInputTokens, + entusagelog.FieldCacheCreationTokens, + entusagelog.FieldCacheCreation5mTokens, + entusagelog.FieldCacheCreation1hTokens, + entusagelog.FieldReasoningOutputTokens, + entusagelog.FieldInputPrice, + entusagelog.FieldOutputPrice, + entusagelog.FieldCachedInputPrice, + entusagelog.FieldCacheCreationPrice, + entusagelog.FieldCacheCreation1hPrice, + entusagelog.FieldInputCost, + entusagelog.FieldOutputCost, + entusagelog.FieldCachedInputCost, + entusagelog.FieldCacheCreationCost, + entusagelog.FieldTotalCost, + entusagelog.FieldActualCost, + entusagelog.FieldBilledCost, + entusagelog.FieldAccountCost, + entusagelog.FieldRateMultiplier, + entusagelog.FieldSellRate, + entusagelog.FieldAccountRateMultiplier, + entusagelog.FieldServiceTier, + entusagelog.FieldImageSize, + entusagelog.FieldStream, + entusagelog.FieldDurationMs, + entusagelog.FieldFirstTokenMs, + entusagelog.FieldUserAgent, + entusagelog.FieldIPAddress, + entusagelog.FieldEndpoint, + entusagelog.FieldReasoningEffort, + entusagelog.FieldUsageMetadata, + entusagelog.FieldUserIDSnapshot, + entusagelog.FieldUserEmailSnapshot, + entusagelog.FieldCreatedAt, + entusagelog.APIKeyColumn, + entusagelog.AccountColumn, + entusagelog.GroupColumn, + entusagelog.UserColumn, +} + // NewUsageStore 创建使用记录仓储。 func NewUsageStore(db *ent.Client) *UsageStore { return &UsageStore{db: db} @@ -38,11 +84,12 @@ func (s *UsageStore) ListUser(ctx context.Context, userID int64, filter appusage } logs, err := query. + Select(usageLogListFields...). WithUser(). WithAPIKey(). WithAccount(). WithGroup(). - Offset((filter.Page-1)*filter.PageSize). + Offset((filter.Page - 1) * filter.PageSize). Limit(filter.PageSize). Order(ent.Desc(entusagelog.FieldID)). All(ctx) @@ -71,11 +118,12 @@ func (s *UsageStore) ListAdmin(ctx context.Context, filter appusage.ListFilter) } logs, err := query. + Select(usageLogListFields...). WithUser(). WithAPIKey(). WithAccount(). WithGroup(). - Offset((filter.Page-1)*filter.PageSize). + Offset((filter.Page - 1) * filter.PageSize). Limit(filter.PageSize). Order(ent.Desc(entusagelog.FieldID)). All(ctx) @@ -547,9 +595,6 @@ func mapUsageLog(item *ent.UsageLog) appusage.LogRecord { IPAddress: item.IPAddress, Endpoint: item.Endpoint, ReasoningEffort: item.ReasoningEffort, - UsageAttributes: item.UsageAttributes, - UsageMetrics: item.UsageMetrics, - UsageCostDetails: item.UsageCostDetails, UsageMetadata: item.UsageMetadata, CreatedAt: item.CreatedAt.Format(time.RFC3339), } diff --git a/backend/internal/plugin/host_service.go b/backend/internal/plugin/host_service.go index 931738d6..38d79a7d 100644 --- a/backend/internal/plugin/host_service.go +++ b/backend/internal/plugin/host_service.go @@ -1158,6 +1158,8 @@ func (h *HostService) recordHostForwardUsage( calcInput.OutputBillingCostOverride = &override } calc := h.calculator.Calculate(calcInput) + reasoningEffort := resolveReasoningEffort(hostForwardReasoningEffort(req), usage) + usageMetadata := usageMetadataFromSDK(usage, usageValues) h.scheduler.AddWindowCost(ctx, accountID, calc.AccountCost) @@ -1198,14 +1200,11 @@ func (h *HostService) recordHostForwardUsage( ServiceTier: usageValues.ServiceTier, ImageSize: usageValues.ImageSize, Endpoint: req.Path, - ReasoningEffort: resolveReasoningEffort(hostForwardReasoningEffort(req), usage), + ReasoningEffort: reasoningEffort, Stream: req.Stream, DurationMs: duration.Milliseconds(), FirstTokenMs: usageValues.FirstTokenMs, - UsageAttributes: usage.Attributes, - UsageMetrics: usage.Metrics, - UsageCostDetails: usage.CostDetails, - UsageMetadata: usage.Metadata, + UsageMetadata: usageMetadata, } if h.recorder == nil { return 0, nil diff --git a/backend/internal/plugin/image_pricing_test.go b/backend/internal/plugin/image_pricing_test.go index ac94cdc7..2be3922a 100644 --- a/backend/internal/plugin/image_pricing_test.go +++ b/backend/internal/plugin/image_pricing_test.go @@ -9,12 +9,8 @@ import ( func TestImageOutputBillingOverride_UsesConfiguredTier(t *testing.T) { usage := &sdk.Usage{ - Attributes: []sdk.UsageAttribute{ - {Key: "image_size", Value: "1672x941"}, - }, - CostDetails: []sdk.UsageCostDetail{ - {Key: "images", AccountCost: 0.40}, - }, + ImageSize: "1672x941", + OutputCost: 0.40, } settings := map[string]map[string]string{ "openai": { @@ -33,12 +29,8 @@ func TestImageOutputBillingOverride_UsesConfiguredTier(t *testing.T) { func TestImageOutputBillingOverride_FallsBackWhenTierUnset(t *testing.T) { usage := &sdk.Usage{ - Attributes: []sdk.UsageAttribute{ - {Key: "image_size", Value: "3840x2160"}, - }, - CostDetails: []sdk.UsageCostDetail{ - {Key: "images", AccountCost: 0.40}, - }, + ImageSize: "3840x2160", + OutputCost: 0.40, } settings := map[string]map[string]string{ "openai": { diff --git a/backend/internal/plugin/outcome.go b/backend/internal/plugin/outcome.go index becbb15c..0c44d97b 100644 --- a/backend/internal/plugin/outcome.go +++ b/backend/internal/plugin/outcome.go @@ -286,6 +286,8 @@ func (f *Forwarder) recordUsage(c *gin.Context, state *forwardState, execution f calcInput.OutputBillingCostOverride = &override } calc := f.calculator.Calculate(calcInput) + reasoningEffort := resolveReasoningEffort(state.reasoningEffort, usage) + usageMetadata := usageMetadataFromSDK(usage, usageValues) // 窗口费用沿用 account_cost(= total × account_rate),与用户账单解耦。 f.scheduler.AddWindowCost(ctx, state.account.ID, calc.AccountCost) @@ -328,19 +330,14 @@ func (f *Forwarder) recordUsage(c *gin.Context, state *forwardState, execution f UserAgent: c.Request.UserAgent(), IPAddress: c.ClientIP(), Endpoint: state.requestPath, - ReasoningEffort: resolveReasoningEffort(state.reasoningEffort, usage), - UsageAttributes: usage.Attributes, - UsageMetrics: usage.Metrics, - UsageCostDetails: usage.CostDetails, - UsageMetadata: usage.Metadata, + ReasoningEffort: reasoningEffort, + UsageMetadata: usageMetadata, }) } func resolveReasoningEffort(fromRequest string, usage *sdk.Usage) string { - if usage != nil && usage.Metadata != nil { - if effort := normalizeReasoningEffort(usage.Metadata["reasoning_effort"]); effort != "" { - return effort - } + if usage != nil && usage.ReasoningEffort != "" { + return normalizeReasoningEffort(usage.ReasoningEffort) } if fromRequest != "" { return fromRequest diff --git a/backend/internal/plugin/usage_adapter.go b/backend/internal/plugin/usage_adapter.go index 37bb0c48..05a6f124 100644 --- a/backend/internal/plugin/usage_adapter.go +++ b/backend/internal/plugin/usage_adapter.go @@ -15,12 +15,17 @@ type usageSnapshot struct { CacheCreation5mTokens int CacheCreation1hTokens int ReasoningOutputTokens int + TextInputTokens int + ImageInputTokens int + ImageCount int InputPrice float64 OutputPrice float64 CachedInputPrice float64 CacheCreationPrice float64 CacheCreation1hPrice float64 + ImageUnitPrice float64 + ImageUnit string InputCost float64 OutputCost float64 @@ -36,64 +41,34 @@ func usageSnapshotFromSDK(usage *sdk.Usage) usageSnapshot { if usage == nil { return usageSnapshot{} } - snap := usageSnapshot{FirstTokenMs: usage.FirstTokenMs} - - for _, metric := range usage.Metrics { - key := normalizedUsageKey(metric.Key, metric.Kind, metric.Label) - switch key { - case "input_tokens", "input_token", "prompt_tokens", "prompt_token": - snap.InputTokens += int(metric.Value) - case "output_tokens", "output_token", "completion_tokens", "completion_token": - snap.OutputTokens += int(metric.Value) - case "cached_input_tokens", "cached_input_token", "cache_read_tokens", "cache_read_token": - snap.CachedInputTokens += int(metric.Value) - case "cache_creation_tokens", "cache_creation_token": - snap.CacheCreationTokens += int(metric.Value) - case "cache_creation_5m_tokens", "cache_creation_5m_token": - snap.CacheCreation5mTokens += int(metric.Value) - case "cache_creation_1h_tokens", "cache_creation_1h_token": - snap.CacheCreation1hTokens += int(metric.Value) - case "reasoning_output_tokens", "reasoning_tokens", "reasoning_token": - snap.ReasoningOutputTokens += int(metric.Value) - } - } - - for _, detail := range usage.CostDetails { - key := normalizedUsageKey(detail.Key, "", detail.Label) - applyUsageCost(&snap, key, detail.AccountCost) - applyUsagePrice(&snap, key, detail.Metadata) - } - if snap.InputCost+snap.OutputCost+snap.CachedInputCost+snap.CacheCreationCost <= 0 { - accountCost := usage.AccountCost - if accountCost <= 0 { - for _, metric := range usage.Metrics { - accountCost += metric.AccountCost - } - for _, detail := range usage.CostDetails { - accountCost += detail.AccountCost - } - } - snap.InputCost = accountCost - } - - for _, attr := range usage.Attributes { - key := normalizedUsageKey(attr.Key, attr.Kind, attr.Label) - switch key { - case "service_tier", "tier": - if snap.ServiceTier == "" { - snap.ServiceTier = attr.Value - } - case "image_size", "resolution", "size": - if snap.ImageSize == "" { - snap.ImageSize = attr.Value - } - } + snap := usageSnapshot{ + InputTokens: usage.InputTokens, + OutputTokens: usage.OutputTokens, + CachedInputTokens: usage.CachedInputTokens, + CacheCreationTokens: usage.CacheCreationTokens, + CacheCreation5mTokens: usage.CacheCreation5mTokens, + CacheCreation1hTokens: usage.CacheCreation1hTokens, + ReasoningOutputTokens: usage.ReasoningOutputTokens, + TextInputTokens: usage.TextInputTokens, + ImageInputTokens: usage.ImageInputTokens, + ImageCount: usage.ImageCount, + InputPrice: usage.InputPrice, + OutputPrice: usage.OutputPrice, + CachedInputPrice: usage.CachedInputPrice, + CacheCreationPrice: usage.CacheCreationPrice, + CacheCreation1hPrice: usage.CacheCreation1hPrice, + ImageUnitPrice: usage.ImageUnitPrice, + ImageUnit: usage.ImageUnit, + InputCost: usage.InputCost, + OutputCost: usage.OutputCost, + CachedInputCost: usage.CachedInputCost, + CacheCreationCost: usage.CacheCreationCost, + ServiceTier: usage.ServiceTier, + ImageSize: usage.ImageSize, + FirstTokenMs: usage.FirstTokenMs, } if usage.Metadata != nil { - if snap.ServiceTier == "" { - snap.ServiceTier = usage.Metadata["service_tier"] - } if snap.ImageSize == "" { snap.ImageSize = usage.Metadata["image_size"] } @@ -102,58 +77,47 @@ func usageSnapshotFromSDK(usage *sdk.Usage) usageSnapshot { return snap } -func applyUsageCost(snap *usageSnapshot, key string, cost float64) { - if snap == nil || cost <= 0 { - return - } - switch key { - case "input", "input_tokens", "input_token", "prompt_tokens", "prompt_token": - snap.InputCost += cost - case "output", "output_tokens", "output_token", "completion_tokens", "completion_token", - "image", "images", "image_generation", "image_tool": - snap.OutputCost += cost - case "cached_input", "cached_input_tokens", "cached_input_token", "cache_read_tokens", "cache_read_token": - snap.CachedInputCost += cost - case "cache_creation", "cache_creation_tokens", "cache_creation_token", - "cache_creation_5m", "cache_creation_5m_tokens", "cache_creation_5m_token", - "cache_creation_1h", "cache_creation_1h_tokens", "cache_creation_1h_token": - snap.CacheCreationCost += cost +func usageMetadataFromSDK(usage *sdk.Usage, snap usageSnapshot) map[string]string { + meta := map[string]string{} + if usage != nil { + if snap.ImageSize == "" { + putMetadata(meta, "image_size", usage.Metadata["image_size"]) + putMetadata(meta, "image_size", usage.Metadata["resolution"]) + putMetadata(meta, "image_size", usage.Metadata["size"]) + } + if snap.ImageUnit == "" { + putMetadata(meta, "image_unit", usage.Metadata["image_unit"]) + putMetadata(meta, "image_unit", usage.Metadata["unit"]) + } } + + putMetadata(meta, "image_size", snap.ImageSize) + putMetadataInt(meta, "input_text_tokens", snap.TextInputTokens) + putMetadataInt(meta, "input_image_tokens", snap.ImageInputTokens) + putMetadataInt(meta, "images", snap.ImageCount) + putMetadataFloat(meta, "image_unit_price", snap.ImageUnitPrice) + putMetadata(meta, "image_unit", snap.ImageUnit) + return meta } -func applyUsagePrice(snap *usageSnapshot, key string, metadata map[string]string) { - if snap == nil || len(metadata) == 0 { +func putMetadata(meta map[string]string, key, value string) { + value = strings.TrimSpace(value) + if value == "" { return } - price, err := strconv.ParseFloat(strings.TrimSpace(metadata["unit_price"]), 64) - if err != nil || price <= 0 { + meta[key] = value +} + +func putMetadataInt(meta map[string]string, key string, value int) { + if value <= 0 { return } - switch key { - case "input", "input_tokens", "input_token", "prompt_tokens", "prompt_token": - snap.InputPrice = price - case "output", "output_tokens", "output_token", "completion_tokens", "completion_token", - "image", "images", "image_generation", "image_tool": - snap.OutputPrice = price - case "cached_input", "cached_input_tokens", "cached_input_token", "cache_read_tokens", "cache_read_token": - snap.CachedInputPrice = price - case "cache_creation", "cache_creation_tokens", "cache_creation_token", - "cache_creation_5m", "cache_creation_5m_tokens", "cache_creation_5m_token": - snap.CacheCreationPrice = price - case "cache_creation_1h", "cache_creation_1h_tokens", "cache_creation_1h_token": - snap.CacheCreation1hPrice = price - } + meta[key] = strconv.Itoa(value) } -func normalizedUsageKey(parts ...string) string { - for _, part := range parts { - part = strings.TrimSpace(strings.ToLower(part)) - if part == "" { - continue - } - part = strings.ReplaceAll(part, "-", "_") - part = strings.ReplaceAll(part, " ", "_") - return part +func putMetadataFloat(meta map[string]string, key string, value float64) { + if value <= 0 { + return } - return "" + meta[key] = strconv.FormatFloat(value, 'f', -1, 64) } diff --git a/backend/internal/server/dto/usage.go b/backend/internal/server/dto/usage.go index a6c5b38a..7c0258bf 100644 --- a/backend/internal/server/dto/usage.go +++ b/backend/internal/server/dto/usage.go @@ -1,60 +1,55 @@ package dto -import sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" - // UsageLogResp 使用记录响应(reseller / admin scope,包含完整的成本字段) type UsageLogResp struct { - ID int64 `json:"id"` - UserID int64 `json:"user_id"` - UserEmail string `json:"user_email,omitempty"` - UserDeleted bool `json:"user_deleted,omitempty"` - APIKeyID int64 `json:"api_key_id"` - APIKeyName string `json:"api_key_name,omitempty"` - APIKeyHint string `json:"api_key_hint,omitempty"` - APIKeyDeleted bool `json:"api_key_deleted"` - AccountID int64 `json:"account_id"` - AccountName string `json:"account_name,omitempty"` - AccountEmail string `json:"account_email,omitempty"` - GroupID int64 `json:"group_id"` - Platform string `json:"platform"` - Model string `json:"model"` - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` - CachedInputTokens int `json:"cached_input_tokens"` - CacheCreationTokens int `json:"cache_creation_tokens"` - CacheCreation5mTokens int `json:"cache_creation_5m_tokens"` - CacheCreation1hTokens int `json:"cache_creation_1h_tokens"` - ReasoningOutputTokens int `json:"reasoning_output_tokens"` - InputPrice float64 `json:"input_price"` - OutputPrice float64 `json:"output_price"` - CachedInputPrice float64 `json:"cached_input_price"` - CacheCreationPrice float64 `json:"cache_creation_price"` - CacheCreation1hPrice float64 `json:"cache_creation_1h_price"` - InputCost float64 `json:"input_cost"` - OutputCost float64 `json:"output_cost"` - CachedInputCost float64 `json:"cached_input_cost"` - CacheCreationCost float64 `json:"cache_creation_cost"` - TotalCost float64 `json:"total_cost"` - ActualCost float64 `json:"actual_cost"` // 平台真实成本/用户扣费 - BilledCost float64 `json:"billed_cost"` // 客户账面消耗(reseller markup 后的金额) - AccountCost float64 `json:"account_cost"` // 账号实际成本 = total × account_rate - RateMultiplier float64 `json:"rate_multiplier"` // 平台计费倍率快照 - SellRate float64 `json:"sell_rate"` // 销售倍率快照 - AccountRateMultiplier float64 `json:"account_rate_multiplier"` // 账号倍率快照 - ServiceTier string `json:"service_tier,omitempty"` - ImageSize string `json:"image_size,omitempty"` // 图像生成实际出图尺寸("WxH"),非图像请求空 - Stream bool `json:"stream"` - DurationMs int64 `json:"duration_ms"` - FirstTokenMs int64 `json:"first_token_ms"` - UserAgent string `json:"user_agent,omitempty"` - IPAddress string `json:"ip_address,omitempty"` - Endpoint string `json:"endpoint,omitempty"` - ReasoningEffort string `json:"reasoning_effort,omitempty"` // 推理强度档位 - UsageAttributes []sdk.UsageAttribute `json:"usage_attributes,omitempty"` - UsageMetrics []sdk.UsageMetric `json:"usage_metrics,omitempty"` - UsageCostDetails []sdk.UsageCostDetail `json:"usage_cost_details,omitempty"` - UsageMetadata map[string]string `json:"usage_metadata,omitempty"` - CreatedAt string `json:"created_at"` + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + UserEmail string `json:"user_email,omitempty"` + UserDeleted bool `json:"user_deleted,omitempty"` + APIKeyID int64 `json:"api_key_id"` + APIKeyName string `json:"api_key_name,omitempty"` + APIKeyHint string `json:"api_key_hint,omitempty"` + APIKeyDeleted bool `json:"api_key_deleted"` + AccountID int64 `json:"account_id"` + AccountName string `json:"account_name,omitempty"` + AccountEmail string `json:"account_email,omitempty"` + GroupID int64 `json:"group_id"` + Platform string `json:"platform"` + Model string `json:"model"` + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + CachedInputTokens int `json:"cached_input_tokens"` + CacheCreationTokens int `json:"cache_creation_tokens"` + CacheCreation5mTokens int `json:"cache_creation_5m_tokens"` + CacheCreation1hTokens int `json:"cache_creation_1h_tokens"` + ReasoningOutputTokens int `json:"reasoning_output_tokens"` + InputPrice float64 `json:"input_price"` + OutputPrice float64 `json:"output_price"` + CachedInputPrice float64 `json:"cached_input_price"` + CacheCreationPrice float64 `json:"cache_creation_price"` + CacheCreation1hPrice float64 `json:"cache_creation_1h_price"` + InputCost float64 `json:"input_cost"` + OutputCost float64 `json:"output_cost"` + CachedInputCost float64 `json:"cached_input_cost"` + CacheCreationCost float64 `json:"cache_creation_cost"` + TotalCost float64 `json:"total_cost"` + ActualCost float64 `json:"actual_cost"` // 平台真实成本/用户扣费 + BilledCost float64 `json:"billed_cost"` // 客户账面消耗(reseller markup 后的金额) + AccountCost float64 `json:"account_cost"` // 账号实际成本 = total × account_rate + RateMultiplier float64 `json:"rate_multiplier"` // 平台计费倍率快照 + SellRate float64 `json:"sell_rate"` // 销售倍率快照 + AccountRateMultiplier float64 `json:"account_rate_multiplier"` // 账号倍率快照 + ServiceTier string `json:"service_tier,omitempty"` + ImageSize string `json:"image_size,omitempty"` // 图像生成实际出图尺寸("WxH"),非图像请求空 + Stream bool `json:"stream"` + DurationMs int64 `json:"duration_ms"` + FirstTokenMs int64 `json:"first_token_ms"` + UserAgent string `json:"user_agent,omitempty"` + IPAddress string `json:"ip_address,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` // 推理强度档位 + UsageMetadata map[string]string `json:"usage_metadata,omitempty"` + CreatedAt string `json:"created_at"` } // CustomerUsageLogResp 使用记录响应(end customer scope,剥离所有平台真实成本字段) @@ -62,29 +57,27 @@ type UsageLogResp struct { // 当请求来自 end customer(通过 API key 登录拿到的 scoped JWT)时返回此结构, // 不暴露 actual_cost / total_cost / 单价 / rate_multiplier 等会泄漏 reseller 毛利的字段。 type CustomerUsageLogResp struct { - ID int64 `json:"id"` - APIKeyID int64 `json:"api_key_id"` - Platform string `json:"platform"` - Model string `json:"model"` - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` - CachedInputTokens int `json:"cached_input_tokens"` - CacheCreationTokens int `json:"cache_creation_tokens"` - CacheCreation5mTokens int `json:"cache_creation_5m_tokens"` - CacheCreation1hTokens int `json:"cache_creation_1h_tokens"` - ReasoningOutputTokens int `json:"reasoning_output_tokens"` - BilledCost float64 `json:"cost"` // 客户视角:"本次消耗 = X 美元" - ServiceTier string `json:"service_tier,omitempty"` - ImageSize string `json:"image_size,omitempty"` // 图像生成实际出图尺寸("WxH"),非图像请求空 - Stream bool `json:"stream"` - DurationMs int64 `json:"duration_ms"` - FirstTokenMs int64 `json:"first_token_ms"` - Endpoint string `json:"endpoint,omitempty"` - ReasoningEffort string `json:"reasoning_effort,omitempty"` // 推理强度档位 - UsageAttributes []sdk.UsageAttribute `json:"usage_attributes,omitempty"` - UsageMetrics []sdk.UsageMetric `json:"usage_metrics,omitempty"` - UsageMetadata map[string]string `json:"usage_metadata,omitempty"` - CreatedAt string `json:"created_at"` + ID int64 `json:"id"` + APIKeyID int64 `json:"api_key_id"` + Platform string `json:"platform"` + Model string `json:"model"` + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + CachedInputTokens int `json:"cached_input_tokens"` + CacheCreationTokens int `json:"cache_creation_tokens"` + CacheCreation5mTokens int `json:"cache_creation_5m_tokens"` + CacheCreation1hTokens int `json:"cache_creation_1h_tokens"` + ReasoningOutputTokens int `json:"reasoning_output_tokens"` + BilledCost float64 `json:"cost"` // 客户视角:"本次消耗 = X 美元" + ServiceTier string `json:"service_tier,omitempty"` + ImageSize string `json:"image_size,omitempty"` // 图像生成实际出图尺寸("WxH"),非图像请求空 + Stream bool `json:"stream"` + DurationMs int64 `json:"duration_ms"` + FirstTokenMs int64 `json:"first_token_ms"` + Endpoint string `json:"endpoint,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` // 推理强度档位 + UsageMetadata map[string]string `json:"usage_metadata,omitempty"` + CreatedAt string `json:"created_at"` } // UsageQuery 使用记录查询参数 diff --git a/backend/internal/server/handler/usage_handler_mapper.go b/backend/internal/server/handler/usage_handler_mapper.go index 4021944b..48524afc 100644 --- a/backend/internal/server/handler/usage_handler_mapper.go +++ b/backend/internal/server/handler/usage_handler_mapper.go @@ -54,9 +54,6 @@ func toUsageLogResp(record appusage.LogRecord) dto.UsageLogResp { IPAddress: record.IPAddress, Endpoint: record.Endpoint, ReasoningEffort: record.ReasoningEffort, - UsageAttributes: record.UsageAttributes, - UsageMetrics: record.UsageMetrics, - UsageCostDetails: record.UsageCostDetails, UsageMetadata: record.UsageMetadata, CreatedAt: record.CreatedAt, } @@ -86,8 +83,6 @@ func toCustomerUsageLogResp(record appusage.LogRecord) dto.CustomerUsageLogResp FirstTokenMs: record.FirstTokenMs, Endpoint: record.Endpoint, ReasoningEffort: record.ReasoningEffort, - UsageAttributes: record.UsageAttributes, - UsageMetrics: record.UsageMetrics, UsageMetadata: record.UsageMetadata, CreatedAt: record.CreatedAt, } diff --git a/web/src/shared/columns/usageColumns.tsx b/web/src/shared/columns/usageColumns.tsx index b61ce651..0cee0e3f 100644 --- a/web/src/shared/columns/usageColumns.tsx +++ b/web/src/shared/columns/usageColumns.tsx @@ -14,7 +14,7 @@ import { subscribeUsageMetricDetailChange, subscribeUsageModelMetaChange, } from '../../app/plugin-frontend-registry'; -import type { UsageLogResp, CustomerUsageLogResp, UsageAttribute, UsageMetric } from '../types'; +import type { UsageLogResp, CustomerUsageLogResp } from '../types'; import { USAGE_TOKEN_COLORS } from '../constants'; import { CostValue } from '../components/CostValue'; @@ -245,24 +245,10 @@ function normalizeUsageKey(value?: string): string { return (value || '').trim().toLowerCase().replace(/[\s-]+/g, '_'); } -function normalizeMetricKey(metric: Pick): string { - return normalizeUsageKey(metric.key || metric.kind || metric.label); -} - function metricNumber(value: unknown): number { return typeof value === 'number' && Number.isFinite(value) ? value : 0; } -function metricMatches(metric: UsageMetric, keys: string[]) { - const key = normalizeMetricKey(metric); - return keys.includes(key); -} - -function metricValue(metrics: UsageMetric[], keys: string[]): number | undefined { - const item = metrics.find((metric) => metricMatches(metric, keys)); - return item ? metricNumber(item.value) : undefined; -} - function firstText(...values: unknown[]): string | undefined { for (const value of values) { if (typeof value !== 'string') continue; @@ -272,14 +258,6 @@ function firstText(...values: unknown[]): string | undefined { return undefined; } -function usageAttributeValue(attributes: UsageAttribute[], keys: string[]): string | undefined { - const normalizedKeys = new Set(keys.map(normalizeUsageKey)); - const item = attributes.find((attr) => ( - normalizedKeys.has(normalizeUsageKey(attr.key || attr.kind || attr.label)) - )); - return firstText(item?.value); -} - function usageMetadataValue(metadata: Record, keys: string[]): string | undefined { const normalizedKeys = new Set(keys.map(normalizeUsageKey)); for (const [key, value] of Object.entries(metadata)) { @@ -290,16 +268,31 @@ function usageMetadataValue(metadata: Record, keys: string[]): s return undefined; } -function isTotalMetric(metric: UsageMetric) { - return metricMatches(metric, ['total_tokens', 'total_token', 'total']); +function usageMetadataNumber(metadata: Record, keys: string[]): number { + const value = usageMetadataValue(metadata, keys); + if (!value) return 0; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; } -function isReasoningMetric(metric: UsageMetric) { - return metricMatches(metric, ['reasoning_output_tokens', 'reasoning_tokens', 'reasoning_token']); +interface MetricDisplay { + key: string; + label: string; + kind: 'token' | 'image'; + unit: string; + value: number; +} + +function isTotalMetric(metric: MetricDisplay) { + return metric.key === 'total_tokens'; +} + +function isReasoningMetric(metric: MetricDisplay) { + return metric.key === 'reasoning_output_tokens'; } -function isOutputMetric(metric: UsageMetric) { - return metricMatches(metric, ['output_tokens', 'output_token', 'completion_tokens', 'completion_token']); +function isOutputMetric(metric: MetricDisplay) { + return metric.key === 'output_tokens'; } function isTokenUnit(unit?: string) { @@ -313,7 +306,7 @@ function formatMetricNumber(value: number): string { : value.toLocaleString(undefined, { maximumFractionDigits: 4 }); } -function formatMetricValue(metric: UsageMetric): string { +function formatMetricValue(metric: MetricDisplay): string { const value = metricNumber(metric.value); const formatted = formatMetricNumber(value); const unit = metric.unit?.trim(); @@ -321,8 +314,8 @@ function formatMetricValue(metric: UsageMetric): string { return `${formatted} ${unit}`; } -function metricColor(metric: UsageMetric, index: number): string | undefined { - const key = normalizeMetricKey(metric); +function metricColor(metric: MetricDisplay, index: number): string | undefined { + const key = metric.key; if (key.includes('input') && !key.includes('cached')) return USAGE_TOKEN_COLORS.input; if (key.includes('output')) return USAGE_TOKEN_COLORS.output; if (key.includes('cache_read') || key.includes('cached_input')) return USAGE_TOKEN_COLORS.cacheRead; @@ -331,58 +324,44 @@ function metricColor(metric: UsageMetric, index: number): string | undefined { return [USAGE_TOKEN_COLORS.input, USAGE_TOKEN_COLORS.output, USAGE_TOKEN_COLORS.cacheRead, USAGE_TOKEN_COLORS.cacheCreation][index % 4]; } -function legacyMetrics(row: UsageRow): UsageMetric[] { +function rowMetrics(row: UsageRow): MetricDisplay[] { const cacheCreation = (row as UsageLogResp).cache_creation_tokens ?? 0; - return [ + const metrics: MetricDisplay[] = [ { key: 'input_tokens', label: '输入 Token', kind: 'token', unit: 'token', value: row.input_tokens }, { key: 'output_tokens', label: '输出 Token', kind: 'token', unit: 'token', value: row.output_tokens }, { key: 'cached_input_tokens', label: '缓存读取 Token', kind: 'token', unit: 'token', value: row.cached_input_tokens }, { key: 'cache_creation_tokens', label: '缓存写入 Token', kind: 'token', unit: 'token', value: cacheCreation }, - ].filter((metric) => metric.value > 0 || metric.key === 'input_tokens' || metric.key === 'output_tokens'); -} - -function rowMetrics(row: UsageRow): UsageMetric[] { - const metrics = row.usage_metrics ?? []; - if (metrics.length > 0) return metrics; - return legacyMetrics(row); + ]; + const imageCount = usageMetadataNumber(row.usage_metadata ?? {}, ['images', 'image_count']); + if (imageCount > 0) { + metrics.push({ key: 'images', label: '图片数量', kind: 'image', unit: 'image', value: imageCount }); + } + return metrics.filter((metric) => metric.value > 0 || metric.key === 'input_tokens' || metric.key === 'output_tokens'); } function buildUsageRecordContext(row: UsageRow, customerScope: boolean) { - const usageCostDetails = !customerScope && 'usage_cost_details' in row - ? (row.usage_cost_details ?? []) - : []; - const usageAttributes = row.usage_attributes ?? []; - const usageMetrics = row.usage_metrics ?? []; const usageMetadata = row.usage_metadata ?? {}; const imageSize = firstText( row.image_size, - usageAttributeValue(usageAttributes, ['image_size', 'resolution', 'size']), usageMetadataValue(usageMetadata, ['image_size', 'resolution', 'size']), ); const serviceTier = firstText( row.service_tier, - usageAttributeValue(usageAttributes, ['service_tier', 'tier']), usageMetadataValue(usageMetadata, ['service_tier', 'tier']), ); const reasoningEffort = firstText( (row as Partial).reasoning_effort, - usageAttributeValue(usageAttributes, ['reasoning_effort', 'reasoning']), usageMetadataValue(usageMetadata, ['reasoning_effort', 'reasoning']), ); - const reasoningTokens = - (row as Partial).reasoning_output_tokens - ?? metricValue(usageMetrics, ['reasoning_output_tokens', 'reasoning_tokens', 'reasoning_token']); + const reasoningTokens = (row as Partial).reasoning_output_tokens; + const inputTextTokens = usageMetadataNumber(usageMetadata, ['input_text_tokens', 'text_input_tokens', 'text_tokens']); + const inputImageTokens = usageMetadataNumber(usageMetadata, ['input_image_tokens', 'image_input_tokens', 'image_tokens']); + const images = usageMetadataNumber(usageMetadata, ['images', 'image_count']); const ctx: Record = { record: row, customerScope, - usageAttributes, - usageMetrics, - usageCostDetails, usageMetadata, - usage_attributes: usageAttributes, - usage_metrics: usageMetrics, - usage_cost_details: usageCostDetails, usage_metadata: usageMetadata, // 常用的行级别字段做扁平化,方便插件扩展渲染器直接取值。 model: row.model, @@ -398,6 +377,9 @@ function buildUsageRecordContext(row: UsageRow, customerScope: boolean) { if (typeof reasoningTokens === 'number' && reasoningTokens > 0) { ctx.reasoning_output_tokens = reasoningTokens; } + if (inputTextTokens > 0) ctx.input_text_tokens = inputTextTokens; + if (inputImageTokens > 0) ctx.input_image_tokens = inputImageTokens; + if (images > 0) ctx.images = images; return ctx; } @@ -410,20 +392,15 @@ function buildCostDetailContext(row: UsageLogResp, adminView: boolean) { function GenericMetricDetail({ row, t }: { row: UsageRow; t: TFunction }) { const allMetrics = rowMetrics(row); - const hasSDKMetrics = (row.usage_metrics?.length ?? 0) > 0; - const reasoningTokens = metricValue(allMetrics, ['reasoning_output_tokens', 'reasoning_tokens', 'reasoning_token']) - ?? (row as Partial).reasoning_output_tokens - ?? 0; + const reasoningTokens = (row as Partial).reasoning_output_tokens ?? 0; const metrics = allMetrics.filter((metric) => ( !isTotalMetric(metric) && !isReasoningMetric(metric) - && (metricNumber(metric.value) > 0 || !hasSDKMetrics || (isOutputMetric(metric) && reasoningTokens > 0)) + && (metricNumber(metric.value) > 0 || metric.key === 'input_tokens' || metric.key === 'output_tokens' || (isOutputMetric(metric) && reasoningTokens > 0)) )); - const totalMetric = allMetrics.find(isTotalMetric); const tokenTotal = - totalMetric?.value - ?? row.input_tokens + row.output_tokens + row.cached_input_tokens + ((row as UsageLogResp).cache_creation_tokens ?? 0); - const shouldShowTokenTotal = !!totalMetric || tokenTotal > 0 || metrics.some((metric) => metric.kind === 'token'); + row.input_tokens + row.output_tokens + row.cached_input_tokens + ((row as UsageLogResp).cache_creation_tokens ?? 0); + const shouldShowTokenTotal = tokenTotal > 0 || metrics.some((metric) => metric.kind === 'token'); return ( @@ -655,15 +632,13 @@ export function useUsageColumns(opts?: { customerScope?: boolean; adminView?: bo title: t('usage.metrics', '计量'), width: '220px', render: (row) => { - const metrics = rowMetrics(row); const PluginUsageMetricDetail = getPluginUsageMetricDetail(row.platform); - const inputTokens = metricValue(metrics, ['input_tokens', 'input_token', 'prompt_tokens', 'prompt_token']) ?? row.input_tokens; - const outputTokens = metricValue(metrics, ['output_tokens', 'output_token', 'completion_tokens', 'completion_token']) ?? row.output_tokens; - const cacheReadTokens = metricValue(metrics, ['cached_input_tokens', 'cached_input_token', 'cache_read_tokens', 'cache_read_token']) ?? row.cached_input_tokens; - const cacheCreationTokens = metricValue(metrics, ['cache_creation_tokens', 'cache_creation_token']) ?? ((row as UsageLogResp).cache_creation_tokens ?? 0); - const total = - metricValue(metrics, ['total_tokens', 'total_token']) - ?? inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens; + const metrics = rowMetrics(row); + const inputTokens = row.input_tokens; + const outputTokens = row.output_tokens; + const cacheReadTokens = row.cached_input_tokens; + const cacheCreationTokens = (row as UsageLogResp).cache_creation_tokens ?? 0; + const total = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens; const hasCacheRead = cacheReadTokens > 0; const hasCacheWrite = cacheCreationTokens > 0; const tokenSummaryVisible = inputTokens > 0 || outputTokens > 0 || hasCacheRead || hasCacheWrite || total > 0; diff --git a/web/src/shared/types/index.ts b/web/src/shared/types/index.ts index 171402c9..4f3a5d2e 100644 --- a/web/src/shared/types/index.ts +++ b/web/src/shared/types/index.ts @@ -433,35 +433,6 @@ export interface AdjustSubscriptionReq { // ==================== Usage ==================== -export interface UsageAttribute { - key?: string; - label: string; - kind?: string; - value: string; - metadata?: Record; -} - -export interface UsageMetric { - key?: string; - label: string; - kind?: string; - unit?: string; - value: number; - account_cost?: number; - currency?: string; - metadata?: Record; -} - -export interface UsageCostDetail { - key?: string; - label: string; - account_cost: number; - user_cost?: number; - billing_multiplier?: number; - currency?: string; - metadata?: Record; -} - export interface UsageLogResp { id: number; user_id: number; @@ -520,9 +491,6 @@ export interface UsageLogResp { endpoint?: string; /** 推理强度档位 */ reasoning_effort?: string; - usage_attributes?: UsageAttribute[]; - usage_metrics?: UsageMetric[]; - usage_cost_details?: UsageCostDetail[]; usage_metadata?: Record; created_at: string; } @@ -560,8 +528,6 @@ export interface CustomerUsageLogResp { endpoint?: string; /** 推理强度档位 */ reasoning_effort?: string; - usage_attributes?: UsageAttribute[]; - usage_metrics?: UsageMetric[]; usage_metadata?: Record; created_at: string; } From 26b06a3d91dfc6bf4e49fb75ead30ac802a141d8 Mon Sep 17 00:00:00 2001 From: quantal Date: Thu, 28 May 2026 00:40:21 +0800 Subject: [PATCH 07/19] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E7=94=A8?= =?UTF-8?q?=E9=87=8F=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/ent/migrate/schema.go | 15 +- backend/ent/mutation.go | 732 ++---------------- backend/ent/runtime.go | 70 +- backend/ent/schema/usagelog.go | 13 - backend/ent/usagelog.go | 92 +-- backend/ent/usagelog/usagelog.go | 49 -- backend/ent/usagelog/where.go | 235 ------ backend/ent/usagelog_create.go | 131 ---- backend/ent/usagelog_update.go | 372 --------- backend/internal/app/usage/types.go | 4 - backend/internal/billing/recorder.go | 8 - backend/internal/bootstrap/startup.go | 25 + backend/internal/infra/store/usage_store.go | 8 - backend/internal/plugin/host_service.go | 4 - backend/internal/plugin/image_pricing_test.go | 57 +- backend/internal/plugin/outcome.go | 4 - backend/internal/plugin/usage_adapter.go | 102 ++- backend/internal/server/dto/usage.go | 7 - .../server/handler/usage_handler_mapper.go | 7 - web/src/shared/columns/usageColumns.tsx | 29 +- web/src/shared/types/index.ts | 17 +- 21 files changed, 260 insertions(+), 1721 deletions(-) diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index 71ef020b..d5c81951 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -275,14 +275,11 @@ var ( {Name: "output_tokens", Type: field.TypeInt, Default: 0}, {Name: "cached_input_tokens", Type: field.TypeInt, Default: 0}, {Name: "cache_creation_tokens", Type: field.TypeInt, Default: 0}, - {Name: "cache_creation_5m_tokens", Type: field.TypeInt, Default: 0}, - {Name: "cache_creation_1h_tokens", Type: field.TypeInt, Default: 0}, {Name: "reasoning_output_tokens", Type: field.TypeInt, Default: 0}, {Name: "input_price", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "output_price", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "cached_input_price", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "cache_creation_price", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, - {Name: "cache_creation_1h_price", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "input_cost", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "output_cost", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "cached_input_cost", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, @@ -295,7 +292,6 @@ var ( {Name: "sell_rate", Type: field.TypeFloat64, Default: 0}, {Name: "account_rate_multiplier", Type: field.TypeFloat64, Default: 1}, {Name: "service_tier", Type: field.TypeString, Default: ""}, - {Name: "image_size", Type: field.TypeString, Default: ""}, {Name: "stream", Type: field.TypeBool, Default: false}, {Name: "duration_ms", Type: field.TypeInt64, Default: 0}, {Name: "first_token_ms", Type: field.TypeInt64, Default: 0}, @@ -303,9 +299,6 @@ var ( {Name: "ip_address", Type: field.TypeString, Default: ""}, {Name: "endpoint", Type: field.TypeString, Default: ""}, {Name: "reasoning_effort", Type: field.TypeString, Default: ""}, - {Name: "usage_attributes", Type: field.TypeJSON, Nullable: true}, - {Name: "usage_metrics", Type: field.TypeJSON, Nullable: true}, - {Name: "usage_cost_details", Type: field.TypeJSON, Nullable: true}, {Name: "usage_metadata", Type: field.TypeJSON, Nullable: true}, {Name: "user_id_snapshot", Type: field.TypeInt, Default: 0}, {Name: "user_email_snapshot", Type: field.TypeString, Default: ""}, @@ -323,25 +316,25 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "usage_logs_api_keys_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[42]}, + Columns: []*schema.Column{UsageLogsColumns[35]}, RefColumns: []*schema.Column{APIKeysColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "usage_logs_accounts_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[43]}, + Columns: []*schema.Column{UsageLogsColumns[36]}, RefColumns: []*schema.Column{AccountsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "usage_logs_groups_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[44]}, + Columns: []*schema.Column{UsageLogsColumns[37]}, RefColumns: []*schema.Column{GroupsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "usage_logs_users_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[45]}, + Columns: []*schema.Column{UsageLogsColumns[38]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.SetNull, }, diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 9498ce4a..63325976 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -24,7 +24,6 @@ import ( "github.com/DouDOU-start/airgate-core/ent/usagelog" "github.com/DouDOU-start/airgate-core/ent/user" "github.com/DouDOU-start/airgate-core/ent/usersubscription" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) const ( @@ -10511,91 +10510,78 @@ func (m *TaskMutation) ResetEdge(name string) error { // UsageLogMutation represents an operation that mutates the UsageLog nodes in the graph. type UsageLogMutation struct { config - op Op - typ string - id *int - platform *string - model *string - input_tokens *int - addinput_tokens *int - output_tokens *int - addoutput_tokens *int - cached_input_tokens *int - addcached_input_tokens *int - cache_creation_tokens *int - addcache_creation_tokens *int - cache_creation_5m_tokens *int - addcache_creation_5m_tokens *int - cache_creation_1h_tokens *int - addcache_creation_1h_tokens *int - reasoning_output_tokens *int - addreasoning_output_tokens *int - input_price *float64 - addinput_price *float64 - output_price *float64 - addoutput_price *float64 - cached_input_price *float64 - addcached_input_price *float64 - cache_creation_price *float64 - addcache_creation_price *float64 - cache_creation_1h_price *float64 - addcache_creation_1h_price *float64 - input_cost *float64 - addinput_cost *float64 - output_cost *float64 - addoutput_cost *float64 - cached_input_cost *float64 - addcached_input_cost *float64 - cache_creation_cost *float64 - addcache_creation_cost *float64 - total_cost *float64 - addtotal_cost *float64 - actual_cost *float64 - addactual_cost *float64 - billed_cost *float64 - addbilled_cost *float64 - account_cost *float64 - addaccount_cost *float64 - rate_multiplier *float64 - addrate_multiplier *float64 - sell_rate *float64 - addsell_rate *float64 - account_rate_multiplier *float64 - addaccount_rate_multiplier *float64 - service_tier *string - image_size *string - stream *bool - duration_ms *int64 - addduration_ms *int64 - first_token_ms *int64 - addfirst_token_ms *int64 - user_agent *string - ip_address *string - endpoint *string - reasoning_effort *string - usage_attributes *[]sdk.UsageAttribute - appendusage_attributes []sdk.UsageAttribute - usage_metrics *[]sdk.UsageMetric - appendusage_metrics []sdk.UsageMetric - usage_cost_details *[]sdk.UsageCostDetail - appendusage_cost_details []sdk.UsageCostDetail - usage_metadata *map[string]string - user_id_snapshot *int - adduser_id_snapshot *int - user_email_snapshot *string - created_at *time.Time - clearedFields map[string]struct{} - user *int - cleareduser bool - api_key *int - clearedapi_key bool - account *int - clearedaccount bool - group *int - clearedgroup bool - done bool - oldValue func(context.Context) (*UsageLog, error) - predicates []predicate.UsageLog + op Op + typ string + id *int + platform *string + model *string + input_tokens *int + addinput_tokens *int + output_tokens *int + addoutput_tokens *int + cached_input_tokens *int + addcached_input_tokens *int + cache_creation_tokens *int + addcache_creation_tokens *int + reasoning_output_tokens *int + addreasoning_output_tokens *int + input_price *float64 + addinput_price *float64 + output_price *float64 + addoutput_price *float64 + cached_input_price *float64 + addcached_input_price *float64 + cache_creation_price *float64 + addcache_creation_price *float64 + input_cost *float64 + addinput_cost *float64 + output_cost *float64 + addoutput_cost *float64 + cached_input_cost *float64 + addcached_input_cost *float64 + cache_creation_cost *float64 + addcache_creation_cost *float64 + total_cost *float64 + addtotal_cost *float64 + actual_cost *float64 + addactual_cost *float64 + billed_cost *float64 + addbilled_cost *float64 + account_cost *float64 + addaccount_cost *float64 + rate_multiplier *float64 + addrate_multiplier *float64 + sell_rate *float64 + addsell_rate *float64 + account_rate_multiplier *float64 + addaccount_rate_multiplier *float64 + service_tier *string + stream *bool + duration_ms *int64 + addduration_ms *int64 + first_token_ms *int64 + addfirst_token_ms *int64 + user_agent *string + ip_address *string + endpoint *string + reasoning_effort *string + usage_metadata *map[string]string + user_id_snapshot *int + adduser_id_snapshot *int + user_email_snapshot *string + created_at *time.Time + clearedFields map[string]struct{} + user *int + cleareduser bool + api_key *int + clearedapi_key bool + account *int + clearedaccount bool + group *int + clearedgroup bool + done bool + oldValue func(context.Context) (*UsageLog, error) + predicates []predicate.UsageLog } var _ ent.Mutation = (*UsageLogMutation)(nil) @@ -10992,118 +10978,6 @@ func (m *UsageLogMutation) ResetCacheCreationTokens() { m.addcache_creation_tokens = nil } -// SetCacheCreation5mTokens sets the "cache_creation_5m_tokens" field. -func (m *UsageLogMutation) SetCacheCreation5mTokens(i int) { - m.cache_creation_5m_tokens = &i - m.addcache_creation_5m_tokens = nil -} - -// CacheCreation5mTokens returns the value of the "cache_creation_5m_tokens" field in the mutation. -func (m *UsageLogMutation) CacheCreation5mTokens() (r int, exists bool) { - v := m.cache_creation_5m_tokens - if v == nil { - return - } - return *v, true -} - -// OldCacheCreation5mTokens returns the old "cache_creation_5m_tokens" field's value of the UsageLog entity. -// If the UsageLog object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageLogMutation) OldCacheCreation5mTokens(ctx context.Context) (v int, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCacheCreation5mTokens is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCacheCreation5mTokens requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldCacheCreation5mTokens: %w", err) - } - return oldValue.CacheCreation5mTokens, nil -} - -// AddCacheCreation5mTokens adds i to the "cache_creation_5m_tokens" field. -func (m *UsageLogMutation) AddCacheCreation5mTokens(i int) { - if m.addcache_creation_5m_tokens != nil { - *m.addcache_creation_5m_tokens += i - } else { - m.addcache_creation_5m_tokens = &i - } -} - -// AddedCacheCreation5mTokens returns the value that was added to the "cache_creation_5m_tokens" field in this mutation. -func (m *UsageLogMutation) AddedCacheCreation5mTokens() (r int, exists bool) { - v := m.addcache_creation_5m_tokens - if v == nil { - return - } - return *v, true -} - -// ResetCacheCreation5mTokens resets all changes to the "cache_creation_5m_tokens" field. -func (m *UsageLogMutation) ResetCacheCreation5mTokens() { - m.cache_creation_5m_tokens = nil - m.addcache_creation_5m_tokens = nil -} - -// SetCacheCreation1hTokens sets the "cache_creation_1h_tokens" field. -func (m *UsageLogMutation) SetCacheCreation1hTokens(i int) { - m.cache_creation_1h_tokens = &i - m.addcache_creation_1h_tokens = nil -} - -// CacheCreation1hTokens returns the value of the "cache_creation_1h_tokens" field in the mutation. -func (m *UsageLogMutation) CacheCreation1hTokens() (r int, exists bool) { - v := m.cache_creation_1h_tokens - if v == nil { - return - } - return *v, true -} - -// OldCacheCreation1hTokens returns the old "cache_creation_1h_tokens" field's value of the UsageLog entity. -// If the UsageLog object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageLogMutation) OldCacheCreation1hTokens(ctx context.Context) (v int, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCacheCreation1hTokens is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCacheCreation1hTokens requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldCacheCreation1hTokens: %w", err) - } - return oldValue.CacheCreation1hTokens, nil -} - -// AddCacheCreation1hTokens adds i to the "cache_creation_1h_tokens" field. -func (m *UsageLogMutation) AddCacheCreation1hTokens(i int) { - if m.addcache_creation_1h_tokens != nil { - *m.addcache_creation_1h_tokens += i - } else { - m.addcache_creation_1h_tokens = &i - } -} - -// AddedCacheCreation1hTokens returns the value that was added to the "cache_creation_1h_tokens" field in this mutation. -func (m *UsageLogMutation) AddedCacheCreation1hTokens() (r int, exists bool) { - v := m.addcache_creation_1h_tokens - if v == nil { - return - } - return *v, true -} - -// ResetCacheCreation1hTokens resets all changes to the "cache_creation_1h_tokens" field. -func (m *UsageLogMutation) ResetCacheCreation1hTokens() { - m.cache_creation_1h_tokens = nil - m.addcache_creation_1h_tokens = nil -} - // SetReasoningOutputTokens sets the "reasoning_output_tokens" field. func (m *UsageLogMutation) SetReasoningOutputTokens(i int) { m.reasoning_output_tokens = &i @@ -11384,62 +11258,6 @@ func (m *UsageLogMutation) ResetCacheCreationPrice() { m.addcache_creation_price = nil } -// SetCacheCreation1hPrice sets the "cache_creation_1h_price" field. -func (m *UsageLogMutation) SetCacheCreation1hPrice(f float64) { - m.cache_creation_1h_price = &f - m.addcache_creation_1h_price = nil -} - -// CacheCreation1hPrice returns the value of the "cache_creation_1h_price" field in the mutation. -func (m *UsageLogMutation) CacheCreation1hPrice() (r float64, exists bool) { - v := m.cache_creation_1h_price - if v == nil { - return - } - return *v, true -} - -// OldCacheCreation1hPrice returns the old "cache_creation_1h_price" field's value of the UsageLog entity. -// If the UsageLog object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageLogMutation) OldCacheCreation1hPrice(ctx context.Context) (v float64, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCacheCreation1hPrice is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCacheCreation1hPrice requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldCacheCreation1hPrice: %w", err) - } - return oldValue.CacheCreation1hPrice, nil -} - -// AddCacheCreation1hPrice adds f to the "cache_creation_1h_price" field. -func (m *UsageLogMutation) AddCacheCreation1hPrice(f float64) { - if m.addcache_creation_1h_price != nil { - *m.addcache_creation_1h_price += f - } else { - m.addcache_creation_1h_price = &f - } -} - -// AddedCacheCreation1hPrice returns the value that was added to the "cache_creation_1h_price" field in this mutation. -func (m *UsageLogMutation) AddedCacheCreation1hPrice() (r float64, exists bool) { - v := m.addcache_creation_1h_price - if v == nil { - return - } - return *v, true -} - -// ResetCacheCreation1hPrice resets all changes to the "cache_creation_1h_price" field. -func (m *UsageLogMutation) ResetCacheCreation1hPrice() { - m.cache_creation_1h_price = nil - m.addcache_creation_1h_price = nil -} - // SetInputCost sets the "input_cost" field. func (m *UsageLogMutation) SetInputCost(f float64) { m.input_cost = &f @@ -12092,42 +11910,6 @@ func (m *UsageLogMutation) ResetServiceTier() { m.service_tier = nil } -// SetImageSize sets the "image_size" field. -func (m *UsageLogMutation) SetImageSize(s string) { - m.image_size = &s -} - -// ImageSize returns the value of the "image_size" field in the mutation. -func (m *UsageLogMutation) ImageSize() (r string, exists bool) { - v := m.image_size - if v == nil { - return - } - return *v, true -} - -// OldImageSize returns the old "image_size" field's value of the UsageLog entity. -// If the UsageLog object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageLogMutation) OldImageSize(ctx context.Context) (v string, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldImageSize is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldImageSize requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldImageSize: %w", err) - } - return oldValue.ImageSize, nil -} - -// ResetImageSize resets all changes to the "image_size" field. -func (m *UsageLogMutation) ResetImageSize() { - m.image_size = nil -} - // SetStream sets the "stream" field. func (m *UsageLogMutation) SetStream(b bool) { m.stream = &b @@ -12420,201 +12202,6 @@ func (m *UsageLogMutation) ResetReasoningEffort() { m.reasoning_effort = nil } -// SetUsageAttributes sets the "usage_attributes" field. -func (m *UsageLogMutation) SetUsageAttributes(sa []sdk.UsageAttribute) { - m.usage_attributes = &sa - m.appendusage_attributes = nil -} - -// UsageAttributes returns the value of the "usage_attributes" field in the mutation. -func (m *UsageLogMutation) UsageAttributes() (r []sdk.UsageAttribute, exists bool) { - v := m.usage_attributes - if v == nil { - return - } - return *v, true -} - -// OldUsageAttributes returns the old "usage_attributes" field's value of the UsageLog entity. -// If the UsageLog object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageLogMutation) OldUsageAttributes(ctx context.Context) (v []sdk.UsageAttribute, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldUsageAttributes is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldUsageAttributes requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldUsageAttributes: %w", err) - } - return oldValue.UsageAttributes, nil -} - -// AppendUsageAttributes adds sa to the "usage_attributes" field. -func (m *UsageLogMutation) AppendUsageAttributes(sa []sdk.UsageAttribute) { - m.appendusage_attributes = append(m.appendusage_attributes, sa...) -} - -// AppendedUsageAttributes returns the list of values that were appended to the "usage_attributes" field in this mutation. -func (m *UsageLogMutation) AppendedUsageAttributes() ([]sdk.UsageAttribute, bool) { - if len(m.appendusage_attributes) == 0 { - return nil, false - } - return m.appendusage_attributes, true -} - -// ClearUsageAttributes clears the value of the "usage_attributes" field. -func (m *UsageLogMutation) ClearUsageAttributes() { - m.usage_attributes = nil - m.appendusage_attributes = nil - m.clearedFields[usagelog.FieldUsageAttributes] = struct{}{} -} - -// UsageAttributesCleared returns if the "usage_attributes" field was cleared in this mutation. -func (m *UsageLogMutation) UsageAttributesCleared() bool { - _, ok := m.clearedFields[usagelog.FieldUsageAttributes] - return ok -} - -// ResetUsageAttributes resets all changes to the "usage_attributes" field. -func (m *UsageLogMutation) ResetUsageAttributes() { - m.usage_attributes = nil - m.appendusage_attributes = nil - delete(m.clearedFields, usagelog.FieldUsageAttributes) -} - -// SetUsageMetrics sets the "usage_metrics" field. -func (m *UsageLogMutation) SetUsageMetrics(sm []sdk.UsageMetric) { - m.usage_metrics = &sm - m.appendusage_metrics = nil -} - -// UsageMetrics returns the value of the "usage_metrics" field in the mutation. -func (m *UsageLogMutation) UsageMetrics() (r []sdk.UsageMetric, exists bool) { - v := m.usage_metrics - if v == nil { - return - } - return *v, true -} - -// OldUsageMetrics returns the old "usage_metrics" field's value of the UsageLog entity. -// If the UsageLog object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageLogMutation) OldUsageMetrics(ctx context.Context) (v []sdk.UsageMetric, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldUsageMetrics is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldUsageMetrics requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldUsageMetrics: %w", err) - } - return oldValue.UsageMetrics, nil -} - -// AppendUsageMetrics adds sm to the "usage_metrics" field. -func (m *UsageLogMutation) AppendUsageMetrics(sm []sdk.UsageMetric) { - m.appendusage_metrics = append(m.appendusage_metrics, sm...) -} - -// AppendedUsageMetrics returns the list of values that were appended to the "usage_metrics" field in this mutation. -func (m *UsageLogMutation) AppendedUsageMetrics() ([]sdk.UsageMetric, bool) { - if len(m.appendusage_metrics) == 0 { - return nil, false - } - return m.appendusage_metrics, true -} - -// ClearUsageMetrics clears the value of the "usage_metrics" field. -func (m *UsageLogMutation) ClearUsageMetrics() { - m.usage_metrics = nil - m.appendusage_metrics = nil - m.clearedFields[usagelog.FieldUsageMetrics] = struct{}{} -} - -// UsageMetricsCleared returns if the "usage_metrics" field was cleared in this mutation. -func (m *UsageLogMutation) UsageMetricsCleared() bool { - _, ok := m.clearedFields[usagelog.FieldUsageMetrics] - return ok -} - -// ResetUsageMetrics resets all changes to the "usage_metrics" field. -func (m *UsageLogMutation) ResetUsageMetrics() { - m.usage_metrics = nil - m.appendusage_metrics = nil - delete(m.clearedFields, usagelog.FieldUsageMetrics) -} - -// SetUsageCostDetails sets the "usage_cost_details" field. -func (m *UsageLogMutation) SetUsageCostDetails(scd []sdk.UsageCostDetail) { - m.usage_cost_details = &scd - m.appendusage_cost_details = nil -} - -// UsageCostDetails returns the value of the "usage_cost_details" field in the mutation. -func (m *UsageLogMutation) UsageCostDetails() (r []sdk.UsageCostDetail, exists bool) { - v := m.usage_cost_details - if v == nil { - return - } - return *v, true -} - -// OldUsageCostDetails returns the old "usage_cost_details" field's value of the UsageLog entity. -// If the UsageLog object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *UsageLogMutation) OldUsageCostDetails(ctx context.Context) (v []sdk.UsageCostDetail, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldUsageCostDetails is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldUsageCostDetails requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldUsageCostDetails: %w", err) - } - return oldValue.UsageCostDetails, nil -} - -// AppendUsageCostDetails adds scd to the "usage_cost_details" field. -func (m *UsageLogMutation) AppendUsageCostDetails(scd []sdk.UsageCostDetail) { - m.appendusage_cost_details = append(m.appendusage_cost_details, scd...) -} - -// AppendedUsageCostDetails returns the list of values that were appended to the "usage_cost_details" field in this mutation. -func (m *UsageLogMutation) AppendedUsageCostDetails() ([]sdk.UsageCostDetail, bool) { - if len(m.appendusage_cost_details) == 0 { - return nil, false - } - return m.appendusage_cost_details, true -} - -// ClearUsageCostDetails clears the value of the "usage_cost_details" field. -func (m *UsageLogMutation) ClearUsageCostDetails() { - m.usage_cost_details = nil - m.appendusage_cost_details = nil - m.clearedFields[usagelog.FieldUsageCostDetails] = struct{}{} -} - -// UsageCostDetailsCleared returns if the "usage_cost_details" field was cleared in this mutation. -func (m *UsageLogMutation) UsageCostDetailsCleared() bool { - _, ok := m.clearedFields[usagelog.FieldUsageCostDetails] - return ok -} - -// ResetUsageCostDetails resets all changes to the "usage_cost_details" field. -func (m *UsageLogMutation) ResetUsageCostDetails() { - m.usage_cost_details = nil - m.appendusage_cost_details = nil - delete(m.clearedFields, usagelog.FieldUsageCostDetails) -} - // SetUsageMetadata sets the "usage_metadata" field. func (m *UsageLogMutation) SetUsageMetadata(value map[string]string) { m.usage_metadata = &value @@ -12982,7 +12569,7 @@ func (m *UsageLogMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *UsageLogMutation) Fields() []string { - fields := make([]string, 0, 41) + fields := make([]string, 0, 34) if m.platform != nil { fields = append(fields, usagelog.FieldPlatform) } @@ -13001,12 +12588,6 @@ func (m *UsageLogMutation) Fields() []string { if m.cache_creation_tokens != nil { fields = append(fields, usagelog.FieldCacheCreationTokens) } - if m.cache_creation_5m_tokens != nil { - fields = append(fields, usagelog.FieldCacheCreation5mTokens) - } - if m.cache_creation_1h_tokens != nil { - fields = append(fields, usagelog.FieldCacheCreation1hTokens) - } if m.reasoning_output_tokens != nil { fields = append(fields, usagelog.FieldReasoningOutputTokens) } @@ -13022,9 +12603,6 @@ func (m *UsageLogMutation) Fields() []string { if m.cache_creation_price != nil { fields = append(fields, usagelog.FieldCacheCreationPrice) } - if m.cache_creation_1h_price != nil { - fields = append(fields, usagelog.FieldCacheCreation1hPrice) - } if m.input_cost != nil { fields = append(fields, usagelog.FieldInputCost) } @@ -13061,9 +12639,6 @@ func (m *UsageLogMutation) Fields() []string { if m.service_tier != nil { fields = append(fields, usagelog.FieldServiceTier) } - if m.image_size != nil { - fields = append(fields, usagelog.FieldImageSize) - } if m.stream != nil { fields = append(fields, usagelog.FieldStream) } @@ -13085,15 +12660,6 @@ func (m *UsageLogMutation) Fields() []string { if m.reasoning_effort != nil { fields = append(fields, usagelog.FieldReasoningEffort) } - if m.usage_attributes != nil { - fields = append(fields, usagelog.FieldUsageAttributes) - } - if m.usage_metrics != nil { - fields = append(fields, usagelog.FieldUsageMetrics) - } - if m.usage_cost_details != nil { - fields = append(fields, usagelog.FieldUsageCostDetails) - } if m.usage_metadata != nil { fields = append(fields, usagelog.FieldUsageMetadata) } @@ -13126,10 +12692,6 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) { return m.CachedInputTokens() case usagelog.FieldCacheCreationTokens: return m.CacheCreationTokens() - case usagelog.FieldCacheCreation5mTokens: - return m.CacheCreation5mTokens() - case usagelog.FieldCacheCreation1hTokens: - return m.CacheCreation1hTokens() case usagelog.FieldReasoningOutputTokens: return m.ReasoningOutputTokens() case usagelog.FieldInputPrice: @@ -13140,8 +12702,6 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) { return m.CachedInputPrice() case usagelog.FieldCacheCreationPrice: return m.CacheCreationPrice() - case usagelog.FieldCacheCreation1hPrice: - return m.CacheCreation1hPrice() case usagelog.FieldInputCost: return m.InputCost() case usagelog.FieldOutputCost: @@ -13166,8 +12726,6 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) { return m.AccountRateMultiplier() case usagelog.FieldServiceTier: return m.ServiceTier() - case usagelog.FieldImageSize: - return m.ImageSize() case usagelog.FieldStream: return m.Stream() case usagelog.FieldDurationMs: @@ -13182,12 +12740,6 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) { return m.Endpoint() case usagelog.FieldReasoningEffort: return m.ReasoningEffort() - case usagelog.FieldUsageAttributes: - return m.UsageAttributes() - case usagelog.FieldUsageMetrics: - return m.UsageMetrics() - case usagelog.FieldUsageCostDetails: - return m.UsageCostDetails() case usagelog.FieldUsageMetadata: return m.UsageMetadata() case usagelog.FieldUserIDSnapshot: @@ -13217,10 +12769,6 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value return m.OldCachedInputTokens(ctx) case usagelog.FieldCacheCreationTokens: return m.OldCacheCreationTokens(ctx) - case usagelog.FieldCacheCreation5mTokens: - return m.OldCacheCreation5mTokens(ctx) - case usagelog.FieldCacheCreation1hTokens: - return m.OldCacheCreation1hTokens(ctx) case usagelog.FieldReasoningOutputTokens: return m.OldReasoningOutputTokens(ctx) case usagelog.FieldInputPrice: @@ -13231,8 +12779,6 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value return m.OldCachedInputPrice(ctx) case usagelog.FieldCacheCreationPrice: return m.OldCacheCreationPrice(ctx) - case usagelog.FieldCacheCreation1hPrice: - return m.OldCacheCreation1hPrice(ctx) case usagelog.FieldInputCost: return m.OldInputCost(ctx) case usagelog.FieldOutputCost: @@ -13257,8 +12803,6 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value return m.OldAccountRateMultiplier(ctx) case usagelog.FieldServiceTier: return m.OldServiceTier(ctx) - case usagelog.FieldImageSize: - return m.OldImageSize(ctx) case usagelog.FieldStream: return m.OldStream(ctx) case usagelog.FieldDurationMs: @@ -13273,12 +12817,6 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value return m.OldEndpoint(ctx) case usagelog.FieldReasoningEffort: return m.OldReasoningEffort(ctx) - case usagelog.FieldUsageAttributes: - return m.OldUsageAttributes(ctx) - case usagelog.FieldUsageMetrics: - return m.OldUsageMetrics(ctx) - case usagelog.FieldUsageCostDetails: - return m.OldUsageCostDetails(ctx) case usagelog.FieldUsageMetadata: return m.OldUsageMetadata(ctx) case usagelog.FieldUserIDSnapshot: @@ -13338,20 +12876,6 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error { } m.SetCacheCreationTokens(v) return nil - case usagelog.FieldCacheCreation5mTokens: - v, ok := value.(int) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetCacheCreation5mTokens(v) - return nil - case usagelog.FieldCacheCreation1hTokens: - v, ok := value.(int) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetCacheCreation1hTokens(v) - return nil case usagelog.FieldReasoningOutputTokens: v, ok := value.(int) if !ok { @@ -13387,13 +12911,6 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error { } m.SetCacheCreationPrice(v) return nil - case usagelog.FieldCacheCreation1hPrice: - v, ok := value.(float64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetCacheCreation1hPrice(v) - return nil case usagelog.FieldInputCost: v, ok := value.(float64) if !ok { @@ -13478,13 +12995,6 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error { } m.SetServiceTier(v) return nil - case usagelog.FieldImageSize: - v, ok := value.(string) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetImageSize(v) - return nil case usagelog.FieldStream: v, ok := value.(bool) if !ok { @@ -13534,27 +13044,6 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error { } m.SetReasoningEffort(v) return nil - case usagelog.FieldUsageAttributes: - v, ok := value.([]sdk.UsageAttribute) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetUsageAttributes(v) - return nil - case usagelog.FieldUsageMetrics: - v, ok := value.([]sdk.UsageMetric) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetUsageMetrics(v) - return nil - case usagelog.FieldUsageCostDetails: - v, ok := value.([]sdk.UsageCostDetail) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetUsageCostDetails(v) - return nil case usagelog.FieldUsageMetadata: v, ok := value.(map[string]string) if !ok { @@ -13603,12 +13092,6 @@ func (m *UsageLogMutation) AddedFields() []string { if m.addcache_creation_tokens != nil { fields = append(fields, usagelog.FieldCacheCreationTokens) } - if m.addcache_creation_5m_tokens != nil { - fields = append(fields, usagelog.FieldCacheCreation5mTokens) - } - if m.addcache_creation_1h_tokens != nil { - fields = append(fields, usagelog.FieldCacheCreation1hTokens) - } if m.addreasoning_output_tokens != nil { fields = append(fields, usagelog.FieldReasoningOutputTokens) } @@ -13624,9 +13107,6 @@ func (m *UsageLogMutation) AddedFields() []string { if m.addcache_creation_price != nil { fields = append(fields, usagelog.FieldCacheCreationPrice) } - if m.addcache_creation_1h_price != nil { - fields = append(fields, usagelog.FieldCacheCreation1hPrice) - } if m.addinput_cost != nil { fields = append(fields, usagelog.FieldInputCost) } @@ -13685,10 +13165,6 @@ func (m *UsageLogMutation) AddedField(name string) (ent.Value, bool) { return m.AddedCachedInputTokens() case usagelog.FieldCacheCreationTokens: return m.AddedCacheCreationTokens() - case usagelog.FieldCacheCreation5mTokens: - return m.AddedCacheCreation5mTokens() - case usagelog.FieldCacheCreation1hTokens: - return m.AddedCacheCreation1hTokens() case usagelog.FieldReasoningOutputTokens: return m.AddedReasoningOutputTokens() case usagelog.FieldInputPrice: @@ -13699,8 +13175,6 @@ func (m *UsageLogMutation) AddedField(name string) (ent.Value, bool) { return m.AddedCachedInputPrice() case usagelog.FieldCacheCreationPrice: return m.AddedCacheCreationPrice() - case usagelog.FieldCacheCreation1hPrice: - return m.AddedCacheCreation1hPrice() case usagelog.FieldInputCost: return m.AddedInputCost() case usagelog.FieldOutputCost: @@ -13766,20 +13240,6 @@ func (m *UsageLogMutation) AddField(name string, value ent.Value) error { } m.AddCacheCreationTokens(v) return nil - case usagelog.FieldCacheCreation5mTokens: - v, ok := value.(int) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.AddCacheCreation5mTokens(v) - return nil - case usagelog.FieldCacheCreation1hTokens: - v, ok := value.(int) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.AddCacheCreation1hTokens(v) - return nil case usagelog.FieldReasoningOutputTokens: v, ok := value.(int) if !ok { @@ -13815,13 +13275,6 @@ func (m *UsageLogMutation) AddField(name string, value ent.Value) error { } m.AddCacheCreationPrice(v) return nil - case usagelog.FieldCacheCreation1hPrice: - v, ok := value.(float64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.AddCacheCreation1hPrice(v) - return nil case usagelog.FieldInputCost: v, ok := value.(float64) if !ok { @@ -13928,15 +13381,6 @@ func (m *UsageLogMutation) AddField(name string, value ent.Value) error { // mutation. func (m *UsageLogMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(usagelog.FieldUsageAttributes) { - fields = append(fields, usagelog.FieldUsageAttributes) - } - if m.FieldCleared(usagelog.FieldUsageMetrics) { - fields = append(fields, usagelog.FieldUsageMetrics) - } - if m.FieldCleared(usagelog.FieldUsageCostDetails) { - fields = append(fields, usagelog.FieldUsageCostDetails) - } if m.FieldCleared(usagelog.FieldUsageMetadata) { fields = append(fields, usagelog.FieldUsageMetadata) } @@ -13954,15 +13398,6 @@ func (m *UsageLogMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *UsageLogMutation) ClearField(name string) error { switch name { - case usagelog.FieldUsageAttributes: - m.ClearUsageAttributes() - return nil - case usagelog.FieldUsageMetrics: - m.ClearUsageMetrics() - return nil - case usagelog.FieldUsageCostDetails: - m.ClearUsageCostDetails() - return nil case usagelog.FieldUsageMetadata: m.ClearUsageMetadata() return nil @@ -13992,12 +13427,6 @@ func (m *UsageLogMutation) ResetField(name string) error { case usagelog.FieldCacheCreationTokens: m.ResetCacheCreationTokens() return nil - case usagelog.FieldCacheCreation5mTokens: - m.ResetCacheCreation5mTokens() - return nil - case usagelog.FieldCacheCreation1hTokens: - m.ResetCacheCreation1hTokens() - return nil case usagelog.FieldReasoningOutputTokens: m.ResetReasoningOutputTokens() return nil @@ -14013,9 +13442,6 @@ func (m *UsageLogMutation) ResetField(name string) error { case usagelog.FieldCacheCreationPrice: m.ResetCacheCreationPrice() return nil - case usagelog.FieldCacheCreation1hPrice: - m.ResetCacheCreation1hPrice() - return nil case usagelog.FieldInputCost: m.ResetInputCost() return nil @@ -14052,9 +13478,6 @@ func (m *UsageLogMutation) ResetField(name string) error { case usagelog.FieldServiceTier: m.ResetServiceTier() return nil - case usagelog.FieldImageSize: - m.ResetImageSize() - return nil case usagelog.FieldStream: m.ResetStream() return nil @@ -14076,15 +13499,6 @@ func (m *UsageLogMutation) ResetField(name string) error { case usagelog.FieldReasoningEffort: m.ResetReasoningEffort() return nil - case usagelog.FieldUsageAttributes: - m.ResetUsageAttributes() - return nil - case usagelog.FieldUsageMetrics: - m.ResetUsageMetrics() - return nil - case usagelog.FieldUsageCostDetails: - m.ResetUsageCostDetails() - return nil case usagelog.FieldUsageMetadata: m.ResetUsageMetadata() return nil diff --git a/backend/ent/runtime.go b/backend/ent/runtime.go index 1d88857f..ef82065d 100644 --- a/backend/ent/runtime.go +++ b/backend/ent/runtime.go @@ -424,128 +424,112 @@ func init() { usagelogDescCacheCreationTokens := usagelogFields[5].Descriptor() // usagelog.DefaultCacheCreationTokens holds the default value on creation for the cache_creation_tokens field. usagelog.DefaultCacheCreationTokens = usagelogDescCacheCreationTokens.Default.(int) - // usagelogDescCacheCreation5mTokens is the schema descriptor for cache_creation_5m_tokens field. - usagelogDescCacheCreation5mTokens := usagelogFields[6].Descriptor() - // usagelog.DefaultCacheCreation5mTokens holds the default value on creation for the cache_creation_5m_tokens field. - usagelog.DefaultCacheCreation5mTokens = usagelogDescCacheCreation5mTokens.Default.(int) - // usagelogDescCacheCreation1hTokens is the schema descriptor for cache_creation_1h_tokens field. - usagelogDescCacheCreation1hTokens := usagelogFields[7].Descriptor() - // usagelog.DefaultCacheCreation1hTokens holds the default value on creation for the cache_creation_1h_tokens field. - usagelog.DefaultCacheCreation1hTokens = usagelogDescCacheCreation1hTokens.Default.(int) // usagelogDescReasoningOutputTokens is the schema descriptor for reasoning_output_tokens field. - usagelogDescReasoningOutputTokens := usagelogFields[8].Descriptor() + usagelogDescReasoningOutputTokens := usagelogFields[6].Descriptor() // usagelog.DefaultReasoningOutputTokens holds the default value on creation for the reasoning_output_tokens field. usagelog.DefaultReasoningOutputTokens = usagelogDescReasoningOutputTokens.Default.(int) // usagelogDescInputPrice is the schema descriptor for input_price field. - usagelogDescInputPrice := usagelogFields[9].Descriptor() + usagelogDescInputPrice := usagelogFields[7].Descriptor() // usagelog.DefaultInputPrice holds the default value on creation for the input_price field. usagelog.DefaultInputPrice = usagelogDescInputPrice.Default.(float64) // usagelogDescOutputPrice is the schema descriptor for output_price field. - usagelogDescOutputPrice := usagelogFields[10].Descriptor() + usagelogDescOutputPrice := usagelogFields[8].Descriptor() // usagelog.DefaultOutputPrice holds the default value on creation for the output_price field. usagelog.DefaultOutputPrice = usagelogDescOutputPrice.Default.(float64) // usagelogDescCachedInputPrice is the schema descriptor for cached_input_price field. - usagelogDescCachedInputPrice := usagelogFields[11].Descriptor() + usagelogDescCachedInputPrice := usagelogFields[9].Descriptor() // usagelog.DefaultCachedInputPrice holds the default value on creation for the cached_input_price field. usagelog.DefaultCachedInputPrice = usagelogDescCachedInputPrice.Default.(float64) // usagelogDescCacheCreationPrice is the schema descriptor for cache_creation_price field. - usagelogDescCacheCreationPrice := usagelogFields[12].Descriptor() + usagelogDescCacheCreationPrice := usagelogFields[10].Descriptor() // usagelog.DefaultCacheCreationPrice holds the default value on creation for the cache_creation_price field. usagelog.DefaultCacheCreationPrice = usagelogDescCacheCreationPrice.Default.(float64) - // usagelogDescCacheCreation1hPrice is the schema descriptor for cache_creation_1h_price field. - usagelogDescCacheCreation1hPrice := usagelogFields[13].Descriptor() - // usagelog.DefaultCacheCreation1hPrice holds the default value on creation for the cache_creation_1h_price field. - usagelog.DefaultCacheCreation1hPrice = usagelogDescCacheCreation1hPrice.Default.(float64) // usagelogDescInputCost is the schema descriptor for input_cost field. - usagelogDescInputCost := usagelogFields[14].Descriptor() + usagelogDescInputCost := usagelogFields[11].Descriptor() // usagelog.DefaultInputCost holds the default value on creation for the input_cost field. usagelog.DefaultInputCost = usagelogDescInputCost.Default.(float64) // usagelogDescOutputCost is the schema descriptor for output_cost field. - usagelogDescOutputCost := usagelogFields[15].Descriptor() + usagelogDescOutputCost := usagelogFields[12].Descriptor() // usagelog.DefaultOutputCost holds the default value on creation for the output_cost field. usagelog.DefaultOutputCost = usagelogDescOutputCost.Default.(float64) // usagelogDescCachedInputCost is the schema descriptor for cached_input_cost field. - usagelogDescCachedInputCost := usagelogFields[16].Descriptor() + usagelogDescCachedInputCost := usagelogFields[13].Descriptor() // usagelog.DefaultCachedInputCost holds the default value on creation for the cached_input_cost field. usagelog.DefaultCachedInputCost = usagelogDescCachedInputCost.Default.(float64) // usagelogDescCacheCreationCost is the schema descriptor for cache_creation_cost field. - usagelogDescCacheCreationCost := usagelogFields[17].Descriptor() + usagelogDescCacheCreationCost := usagelogFields[14].Descriptor() // usagelog.DefaultCacheCreationCost holds the default value on creation for the cache_creation_cost field. usagelog.DefaultCacheCreationCost = usagelogDescCacheCreationCost.Default.(float64) // usagelogDescTotalCost is the schema descriptor for total_cost field. - usagelogDescTotalCost := usagelogFields[18].Descriptor() + usagelogDescTotalCost := usagelogFields[15].Descriptor() // usagelog.DefaultTotalCost holds the default value on creation for the total_cost field. usagelog.DefaultTotalCost = usagelogDescTotalCost.Default.(float64) // usagelogDescActualCost is the schema descriptor for actual_cost field. - usagelogDescActualCost := usagelogFields[19].Descriptor() + usagelogDescActualCost := usagelogFields[16].Descriptor() // usagelog.DefaultActualCost holds the default value on creation for the actual_cost field. usagelog.DefaultActualCost = usagelogDescActualCost.Default.(float64) // usagelogDescBilledCost is the schema descriptor for billed_cost field. - usagelogDescBilledCost := usagelogFields[20].Descriptor() + usagelogDescBilledCost := usagelogFields[17].Descriptor() // usagelog.DefaultBilledCost holds the default value on creation for the billed_cost field. usagelog.DefaultBilledCost = usagelogDescBilledCost.Default.(float64) // usagelogDescAccountCost is the schema descriptor for account_cost field. - usagelogDescAccountCost := usagelogFields[21].Descriptor() + usagelogDescAccountCost := usagelogFields[18].Descriptor() // usagelog.DefaultAccountCost holds the default value on creation for the account_cost field. usagelog.DefaultAccountCost = usagelogDescAccountCost.Default.(float64) // usagelogDescRateMultiplier is the schema descriptor for rate_multiplier field. - usagelogDescRateMultiplier := usagelogFields[22].Descriptor() + usagelogDescRateMultiplier := usagelogFields[19].Descriptor() // usagelog.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field. usagelog.DefaultRateMultiplier = usagelogDescRateMultiplier.Default.(float64) // usagelogDescSellRate is the schema descriptor for sell_rate field. - usagelogDescSellRate := usagelogFields[23].Descriptor() + usagelogDescSellRate := usagelogFields[20].Descriptor() // usagelog.DefaultSellRate holds the default value on creation for the sell_rate field. usagelog.DefaultSellRate = usagelogDescSellRate.Default.(float64) // usagelogDescAccountRateMultiplier is the schema descriptor for account_rate_multiplier field. - usagelogDescAccountRateMultiplier := usagelogFields[24].Descriptor() + usagelogDescAccountRateMultiplier := usagelogFields[21].Descriptor() // usagelog.DefaultAccountRateMultiplier holds the default value on creation for the account_rate_multiplier field. usagelog.DefaultAccountRateMultiplier = usagelogDescAccountRateMultiplier.Default.(float64) // usagelogDescServiceTier is the schema descriptor for service_tier field. - usagelogDescServiceTier := usagelogFields[25].Descriptor() + usagelogDescServiceTier := usagelogFields[22].Descriptor() // usagelog.DefaultServiceTier holds the default value on creation for the service_tier field. usagelog.DefaultServiceTier = usagelogDescServiceTier.Default.(string) - // usagelogDescImageSize is the schema descriptor for image_size field. - usagelogDescImageSize := usagelogFields[26].Descriptor() - // usagelog.DefaultImageSize holds the default value on creation for the image_size field. - usagelog.DefaultImageSize = usagelogDescImageSize.Default.(string) // usagelogDescStream is the schema descriptor for stream field. - usagelogDescStream := usagelogFields[27].Descriptor() + usagelogDescStream := usagelogFields[23].Descriptor() // usagelog.DefaultStream holds the default value on creation for the stream field. usagelog.DefaultStream = usagelogDescStream.Default.(bool) // usagelogDescDurationMs is the schema descriptor for duration_ms field. - usagelogDescDurationMs := usagelogFields[28].Descriptor() + usagelogDescDurationMs := usagelogFields[24].Descriptor() // usagelog.DefaultDurationMs holds the default value on creation for the duration_ms field. usagelog.DefaultDurationMs = usagelogDescDurationMs.Default.(int64) // usagelogDescFirstTokenMs is the schema descriptor for first_token_ms field. - usagelogDescFirstTokenMs := usagelogFields[29].Descriptor() + usagelogDescFirstTokenMs := usagelogFields[25].Descriptor() // usagelog.DefaultFirstTokenMs holds the default value on creation for the first_token_ms field. usagelog.DefaultFirstTokenMs = usagelogDescFirstTokenMs.Default.(int64) // usagelogDescUserAgent is the schema descriptor for user_agent field. - usagelogDescUserAgent := usagelogFields[30].Descriptor() + usagelogDescUserAgent := usagelogFields[26].Descriptor() // usagelog.DefaultUserAgent holds the default value on creation for the user_agent field. usagelog.DefaultUserAgent = usagelogDescUserAgent.Default.(string) // usagelogDescIPAddress is the schema descriptor for ip_address field. - usagelogDescIPAddress := usagelogFields[31].Descriptor() + usagelogDescIPAddress := usagelogFields[27].Descriptor() // usagelog.DefaultIPAddress holds the default value on creation for the ip_address field. usagelog.DefaultIPAddress = usagelogDescIPAddress.Default.(string) // usagelogDescEndpoint is the schema descriptor for endpoint field. - usagelogDescEndpoint := usagelogFields[32].Descriptor() + usagelogDescEndpoint := usagelogFields[28].Descriptor() // usagelog.DefaultEndpoint holds the default value on creation for the endpoint field. usagelog.DefaultEndpoint = usagelogDescEndpoint.Default.(string) // usagelogDescReasoningEffort is the schema descriptor for reasoning_effort field. - usagelogDescReasoningEffort := usagelogFields[33].Descriptor() + usagelogDescReasoningEffort := usagelogFields[29].Descriptor() // usagelog.DefaultReasoningEffort holds the default value on creation for the reasoning_effort field. usagelog.DefaultReasoningEffort = usagelogDescReasoningEffort.Default.(string) // usagelogDescUserIDSnapshot is the schema descriptor for user_id_snapshot field. - usagelogDescUserIDSnapshot := usagelogFields[38].Descriptor() + usagelogDescUserIDSnapshot := usagelogFields[31].Descriptor() // usagelog.DefaultUserIDSnapshot holds the default value on creation for the user_id_snapshot field. usagelog.DefaultUserIDSnapshot = usagelogDescUserIDSnapshot.Default.(int) // usagelogDescUserEmailSnapshot is the schema descriptor for user_email_snapshot field. - usagelogDescUserEmailSnapshot := usagelogFields[39].Descriptor() + usagelogDescUserEmailSnapshot := usagelogFields[32].Descriptor() // usagelog.DefaultUserEmailSnapshot holds the default value on creation for the user_email_snapshot field. usagelog.DefaultUserEmailSnapshot = usagelogDescUserEmailSnapshot.Default.(string) // usagelogDescCreatedAt is the schema descriptor for created_at field. - usagelogDescCreatedAt := usagelogFields[40].Descriptor() + usagelogDescCreatedAt := usagelogFields[33].Descriptor() // usagelog.DefaultCreatedAt holds the default value on creation for the created_at field. usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time) userFields := schema.User{}.Fields() diff --git a/backend/ent/schema/usagelog.go b/backend/ent/schema/usagelog.go index 99b3db5f..8919a3c8 100644 --- a/backend/ent/schema/usagelog.go +++ b/backend/ent/schema/usagelog.go @@ -5,8 +5,6 @@ import ( "entgo.io/ent/dialect" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" - - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // UsageLog 使用日志(只追加) @@ -22,8 +20,6 @@ func (UsageLog) Fields() []ent.Field { field.Int("output_tokens").Default(0), field.Int("cached_input_tokens").Default(0), field.Int("cache_creation_tokens").Default(0), - field.Int("cache_creation_5m_tokens").Default(0), - field.Int("cache_creation_1h_tokens").Default(0), field.Int("reasoning_output_tokens").Default(0), field.Float("input_price").Default(0). SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), @@ -33,8 +29,6 @@ func (UsageLog) Fields() []ent.Field { SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), field.Float("cache_creation_price").Default(0). SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), - field.Float("cache_creation_1h_price").Default(0). - SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), field.Float("input_cost").Default(0). SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), field.Float("output_cost").Default(0). @@ -61,9 +55,6 @@ func (UsageLog) Fields() []ent.Field { field.Float("account_rate_multiplier").Default(1.0). Comment("快照:本次请求生效的 account_rate"), field.String("service_tier").Default(""), - // image_size 图像生成请求实际出图尺寸("WxH")。非图像请求留空。 - // admin 后台展示用,让用户能直观看出"为什么这次图扣了 0.40"——按 1K/2K/4K 分档计费。 - field.String("image_size").Default(""), field.Bool("stream").Default(false), field.Int64("duration_ms").Default(0), field.Int64("first_token_ms").Default(0), @@ -73,10 +64,6 @@ func (UsageLog) Fields() []ent.Field { field.String("endpoint").Default(""), // 推理强度档位。 field.String("reasoning_effort").Default(""), - // SDK 原始用量明细:Core 只保存和透出,具体展示由插件前端 slot 负责。 - field.JSON("usage_attributes", []sdk.UsageAttribute{}).Optional(), - field.JSON("usage_metrics", []sdk.UsageMetric{}).Optional(), - field.JSON("usage_cost_details", []sdk.UsageCostDetail{}).Optional(), field.JSON("usage_metadata", map[string]string{}).Optional(), field.Int("user_id_snapshot").Default(0). Comment("用户 ID 快照。用户硬删除后保留历史使用记录与计费归属。"), diff --git a/backend/ent/usagelog.go b/backend/ent/usagelog.go index 938fb4ac..0784ee0f 100644 --- a/backend/ent/usagelog.go +++ b/backend/ent/usagelog.go @@ -15,7 +15,6 @@ import ( "github.com/DouDOU-start/airgate-core/ent/group" "github.com/DouDOU-start/airgate-core/ent/usagelog" "github.com/DouDOU-start/airgate-core/ent/user" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // UsageLog is the model entity for the UsageLog schema. @@ -35,10 +34,6 @@ type UsageLog struct { CachedInputTokens int `json:"cached_input_tokens,omitempty"` // CacheCreationTokens holds the value of the "cache_creation_tokens" field. CacheCreationTokens int `json:"cache_creation_tokens,omitempty"` - // CacheCreation5mTokens holds the value of the "cache_creation_5m_tokens" field. - CacheCreation5mTokens int `json:"cache_creation_5m_tokens,omitempty"` - // CacheCreation1hTokens holds the value of the "cache_creation_1h_tokens" field. - CacheCreation1hTokens int `json:"cache_creation_1h_tokens,omitempty"` // ReasoningOutputTokens holds the value of the "reasoning_output_tokens" field. ReasoningOutputTokens int `json:"reasoning_output_tokens,omitempty"` // InputPrice holds the value of the "input_price" field. @@ -49,8 +44,6 @@ type UsageLog struct { CachedInputPrice float64 `json:"cached_input_price,omitempty"` // CacheCreationPrice holds the value of the "cache_creation_price" field. CacheCreationPrice float64 `json:"cache_creation_price,omitempty"` - // CacheCreation1hPrice holds the value of the "cache_creation_1h_price" field. - CacheCreation1hPrice float64 `json:"cache_creation_1h_price,omitempty"` // InputCost holds the value of the "input_cost" field. InputCost float64 `json:"input_cost,omitempty"` // OutputCost holds the value of the "output_cost" field. @@ -75,8 +68,6 @@ type UsageLog struct { AccountRateMultiplier float64 `json:"account_rate_multiplier,omitempty"` // ServiceTier holds the value of the "service_tier" field. ServiceTier string `json:"service_tier,omitempty"` - // ImageSize holds the value of the "image_size" field. - ImageSize string `json:"image_size,omitempty"` // Stream holds the value of the "stream" field. Stream bool `json:"stream,omitempty"` // DurationMs holds the value of the "duration_ms" field. @@ -91,12 +82,6 @@ type UsageLog struct { Endpoint string `json:"endpoint,omitempty"` // ReasoningEffort holds the value of the "reasoning_effort" field. ReasoningEffort string `json:"reasoning_effort,omitempty"` - // UsageAttributes holds the value of the "usage_attributes" field. - UsageAttributes []sdk.UsageAttribute `json:"usage_attributes,omitempty"` - // UsageMetrics holds the value of the "usage_metrics" field. - UsageMetrics []sdk.UsageMetric `json:"usage_metrics,omitempty"` - // UsageCostDetails holds the value of the "usage_cost_details" field. - UsageCostDetails []sdk.UsageCostDetail `json:"usage_cost_details,omitempty"` // UsageMetadata holds the value of the "usage_metadata" field. UsageMetadata map[string]string `json:"usage_metadata,omitempty"` // 用户 ID 快照。用户硬删除后保留历史使用记录与计费归属。 @@ -179,15 +164,15 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case usagelog.FieldUsageAttributes, usagelog.FieldUsageMetrics, usagelog.FieldUsageCostDetails, usagelog.FieldUsageMetadata: + case usagelog.FieldUsageMetadata: values[i] = new([]byte) case usagelog.FieldStream: values[i] = new(sql.NullBool) - case usagelog.FieldInputPrice, usagelog.FieldOutputPrice, usagelog.FieldCachedInputPrice, usagelog.FieldCacheCreationPrice, usagelog.FieldCacheCreation1hPrice, usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCachedInputCost, usagelog.FieldCacheCreationCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldBilledCost, usagelog.FieldAccountCost, usagelog.FieldRateMultiplier, usagelog.FieldSellRate, usagelog.FieldAccountRateMultiplier: + case usagelog.FieldInputPrice, usagelog.FieldOutputPrice, usagelog.FieldCachedInputPrice, usagelog.FieldCacheCreationPrice, usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCachedInputCost, usagelog.FieldCacheCreationCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldBilledCost, usagelog.FieldAccountCost, usagelog.FieldRateMultiplier, usagelog.FieldSellRate, usagelog.FieldAccountRateMultiplier: values[i] = new(sql.NullFloat64) - case usagelog.FieldID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCachedInputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldReasoningOutputTokens, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldUserIDSnapshot: + case usagelog.FieldID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCachedInputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldReasoningOutputTokens, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldUserIDSnapshot: values[i] = new(sql.NullInt64) - case usagelog.FieldPlatform, usagelog.FieldModel, usagelog.FieldServiceTier, usagelog.FieldImageSize, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldEndpoint, usagelog.FieldReasoningEffort, usagelog.FieldUserEmailSnapshot: + case usagelog.FieldPlatform, usagelog.FieldModel, usagelog.FieldServiceTier, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldEndpoint, usagelog.FieldReasoningEffort, usagelog.FieldUserEmailSnapshot: values[i] = new(sql.NullString) case usagelog.FieldCreatedAt: values[i] = new(sql.NullTime) @@ -256,18 +241,6 @@ func (ul *UsageLog) assignValues(columns []string, values []any) error { } else if value.Valid { ul.CacheCreationTokens = int(value.Int64) } - case usagelog.FieldCacheCreation5mTokens: - if value, ok := values[i].(*sql.NullInt64); !ok { - return fmt.Errorf("unexpected type %T for field cache_creation_5m_tokens", values[i]) - } else if value.Valid { - ul.CacheCreation5mTokens = int(value.Int64) - } - case usagelog.FieldCacheCreation1hTokens: - if value, ok := values[i].(*sql.NullInt64); !ok { - return fmt.Errorf("unexpected type %T for field cache_creation_1h_tokens", values[i]) - } else if value.Valid { - ul.CacheCreation1hTokens = int(value.Int64) - } case usagelog.FieldReasoningOutputTokens: if value, ok := values[i].(*sql.NullInt64); !ok { return fmt.Errorf("unexpected type %T for field reasoning_output_tokens", values[i]) @@ -298,12 +271,6 @@ func (ul *UsageLog) assignValues(columns []string, values []any) error { } else if value.Valid { ul.CacheCreationPrice = value.Float64 } - case usagelog.FieldCacheCreation1hPrice: - if value, ok := values[i].(*sql.NullFloat64); !ok { - return fmt.Errorf("unexpected type %T for field cache_creation_1h_price", values[i]) - } else if value.Valid { - ul.CacheCreation1hPrice = value.Float64 - } case usagelog.FieldInputCost: if value, ok := values[i].(*sql.NullFloat64); !ok { return fmt.Errorf("unexpected type %T for field input_cost", values[i]) @@ -376,12 +343,6 @@ func (ul *UsageLog) assignValues(columns []string, values []any) error { } else if value.Valid { ul.ServiceTier = value.String } - case usagelog.FieldImageSize: - if value, ok := values[i].(*sql.NullString); !ok { - return fmt.Errorf("unexpected type %T for field image_size", values[i]) - } else if value.Valid { - ul.ImageSize = value.String - } case usagelog.FieldStream: if value, ok := values[i].(*sql.NullBool); !ok { return fmt.Errorf("unexpected type %T for field stream", values[i]) @@ -424,30 +385,6 @@ func (ul *UsageLog) assignValues(columns []string, values []any) error { } else if value.Valid { ul.ReasoningEffort = value.String } - case usagelog.FieldUsageAttributes: - if value, ok := values[i].(*[]byte); !ok { - return fmt.Errorf("unexpected type %T for field usage_attributes", values[i]) - } else if value != nil && len(*value) > 0 { - if err := json.Unmarshal(*value, &ul.UsageAttributes); err != nil { - return fmt.Errorf("unmarshal field usage_attributes: %w", err) - } - } - case usagelog.FieldUsageMetrics: - if value, ok := values[i].(*[]byte); !ok { - return fmt.Errorf("unexpected type %T for field usage_metrics", values[i]) - } else if value != nil && len(*value) > 0 { - if err := json.Unmarshal(*value, &ul.UsageMetrics); err != nil { - return fmt.Errorf("unmarshal field usage_metrics: %w", err) - } - } - case usagelog.FieldUsageCostDetails: - if value, ok := values[i].(*[]byte); !ok { - return fmt.Errorf("unexpected type %T for field usage_cost_details", values[i]) - } else if value != nil && len(*value) > 0 { - if err := json.Unmarshal(*value, &ul.UsageCostDetails); err != nil { - return fmt.Errorf("unmarshal field usage_cost_details: %w", err) - } - } case usagelog.FieldUsageMetadata: if value, ok := values[i].(*[]byte); !ok { return fmt.Errorf("unexpected type %T for field usage_metadata", values[i]) @@ -576,12 +513,6 @@ func (ul *UsageLog) String() string { builder.WriteString("cache_creation_tokens=") builder.WriteString(fmt.Sprintf("%v", ul.CacheCreationTokens)) builder.WriteString(", ") - builder.WriteString("cache_creation_5m_tokens=") - builder.WriteString(fmt.Sprintf("%v", ul.CacheCreation5mTokens)) - builder.WriteString(", ") - builder.WriteString("cache_creation_1h_tokens=") - builder.WriteString(fmt.Sprintf("%v", ul.CacheCreation1hTokens)) - builder.WriteString(", ") builder.WriteString("reasoning_output_tokens=") builder.WriteString(fmt.Sprintf("%v", ul.ReasoningOutputTokens)) builder.WriteString(", ") @@ -597,9 +528,6 @@ func (ul *UsageLog) String() string { builder.WriteString("cache_creation_price=") builder.WriteString(fmt.Sprintf("%v", ul.CacheCreationPrice)) builder.WriteString(", ") - builder.WriteString("cache_creation_1h_price=") - builder.WriteString(fmt.Sprintf("%v", ul.CacheCreation1hPrice)) - builder.WriteString(", ") builder.WriteString("input_cost=") builder.WriteString(fmt.Sprintf("%v", ul.InputCost)) builder.WriteString(", ") @@ -636,9 +564,6 @@ func (ul *UsageLog) String() string { builder.WriteString("service_tier=") builder.WriteString(ul.ServiceTier) builder.WriteString(", ") - builder.WriteString("image_size=") - builder.WriteString(ul.ImageSize) - builder.WriteString(", ") builder.WriteString("stream=") builder.WriteString(fmt.Sprintf("%v", ul.Stream)) builder.WriteString(", ") @@ -660,15 +585,6 @@ func (ul *UsageLog) String() string { builder.WriteString("reasoning_effort=") builder.WriteString(ul.ReasoningEffort) builder.WriteString(", ") - builder.WriteString("usage_attributes=") - builder.WriteString(fmt.Sprintf("%v", ul.UsageAttributes)) - builder.WriteString(", ") - builder.WriteString("usage_metrics=") - builder.WriteString(fmt.Sprintf("%v", ul.UsageMetrics)) - builder.WriteString(", ") - builder.WriteString("usage_cost_details=") - builder.WriteString(fmt.Sprintf("%v", ul.UsageCostDetails)) - builder.WriteString(", ") builder.WriteString("usage_metadata=") builder.WriteString(fmt.Sprintf("%v", ul.UsageMetadata)) builder.WriteString(", ") diff --git a/backend/ent/usagelog/usagelog.go b/backend/ent/usagelog/usagelog.go index 95791a42..4f067295 100644 --- a/backend/ent/usagelog/usagelog.go +++ b/backend/ent/usagelog/usagelog.go @@ -26,10 +26,6 @@ const ( FieldCachedInputTokens = "cached_input_tokens" // FieldCacheCreationTokens holds the string denoting the cache_creation_tokens field in the database. FieldCacheCreationTokens = "cache_creation_tokens" - // FieldCacheCreation5mTokens holds the string denoting the cache_creation_5m_tokens field in the database. - FieldCacheCreation5mTokens = "cache_creation_5m_tokens" - // FieldCacheCreation1hTokens holds the string denoting the cache_creation_1h_tokens field in the database. - FieldCacheCreation1hTokens = "cache_creation_1h_tokens" // FieldReasoningOutputTokens holds the string denoting the reasoning_output_tokens field in the database. FieldReasoningOutputTokens = "reasoning_output_tokens" // FieldInputPrice holds the string denoting the input_price field in the database. @@ -40,8 +36,6 @@ const ( FieldCachedInputPrice = "cached_input_price" // FieldCacheCreationPrice holds the string denoting the cache_creation_price field in the database. FieldCacheCreationPrice = "cache_creation_price" - // FieldCacheCreation1hPrice holds the string denoting the cache_creation_1h_price field in the database. - FieldCacheCreation1hPrice = "cache_creation_1h_price" // FieldInputCost holds the string denoting the input_cost field in the database. FieldInputCost = "input_cost" // FieldOutputCost holds the string denoting the output_cost field in the database. @@ -66,8 +60,6 @@ const ( FieldAccountRateMultiplier = "account_rate_multiplier" // FieldServiceTier holds the string denoting the service_tier field in the database. FieldServiceTier = "service_tier" - // FieldImageSize holds the string denoting the image_size field in the database. - FieldImageSize = "image_size" // FieldStream holds the string denoting the stream field in the database. FieldStream = "stream" // FieldDurationMs holds the string denoting the duration_ms field in the database. @@ -82,12 +74,6 @@ const ( FieldEndpoint = "endpoint" // FieldReasoningEffort holds the string denoting the reasoning_effort field in the database. FieldReasoningEffort = "reasoning_effort" - // FieldUsageAttributes holds the string denoting the usage_attributes field in the database. - FieldUsageAttributes = "usage_attributes" - // FieldUsageMetrics holds the string denoting the usage_metrics field in the database. - FieldUsageMetrics = "usage_metrics" - // FieldUsageCostDetails holds the string denoting the usage_cost_details field in the database. - FieldUsageCostDetails = "usage_cost_details" // FieldUsageMetadata holds the string denoting the usage_metadata field in the database. FieldUsageMetadata = "usage_metadata" // FieldUserIDSnapshot holds the string denoting the user_id_snapshot field in the database. @@ -145,14 +131,11 @@ var Columns = []string{ FieldOutputTokens, FieldCachedInputTokens, FieldCacheCreationTokens, - FieldCacheCreation5mTokens, - FieldCacheCreation1hTokens, FieldReasoningOutputTokens, FieldInputPrice, FieldOutputPrice, FieldCachedInputPrice, FieldCacheCreationPrice, - FieldCacheCreation1hPrice, FieldInputCost, FieldOutputCost, FieldCachedInputCost, @@ -165,7 +148,6 @@ var Columns = []string{ FieldSellRate, FieldAccountRateMultiplier, FieldServiceTier, - FieldImageSize, FieldStream, FieldDurationMs, FieldFirstTokenMs, @@ -173,9 +155,6 @@ var Columns = []string{ FieldIPAddress, FieldEndpoint, FieldReasoningEffort, - FieldUsageAttributes, - FieldUsageMetrics, - FieldUsageCostDetails, FieldUsageMetadata, FieldUserIDSnapshot, FieldUserEmailSnapshot, @@ -219,10 +198,6 @@ var ( DefaultCachedInputTokens int // DefaultCacheCreationTokens holds the default value on creation for the "cache_creation_tokens" field. DefaultCacheCreationTokens int - // DefaultCacheCreation5mTokens holds the default value on creation for the "cache_creation_5m_tokens" field. - DefaultCacheCreation5mTokens int - // DefaultCacheCreation1hTokens holds the default value on creation for the "cache_creation_1h_tokens" field. - DefaultCacheCreation1hTokens int // DefaultReasoningOutputTokens holds the default value on creation for the "reasoning_output_tokens" field. DefaultReasoningOutputTokens int // DefaultInputPrice holds the default value on creation for the "input_price" field. @@ -233,8 +208,6 @@ var ( DefaultCachedInputPrice float64 // DefaultCacheCreationPrice holds the default value on creation for the "cache_creation_price" field. DefaultCacheCreationPrice float64 - // DefaultCacheCreation1hPrice holds the default value on creation for the "cache_creation_1h_price" field. - DefaultCacheCreation1hPrice float64 // DefaultInputCost holds the default value on creation for the "input_cost" field. DefaultInputCost float64 // DefaultOutputCost holds the default value on creation for the "output_cost" field. @@ -259,8 +232,6 @@ var ( DefaultAccountRateMultiplier float64 // DefaultServiceTier holds the default value on creation for the "service_tier" field. DefaultServiceTier string - // DefaultImageSize holds the default value on creation for the "image_size" field. - DefaultImageSize string // DefaultStream holds the default value on creation for the "stream" field. DefaultStream bool // DefaultDurationMs holds the default value on creation for the "duration_ms" field. @@ -321,16 +292,6 @@ func ByCacheCreationTokens(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCacheCreationTokens, opts...).ToFunc() } -// ByCacheCreation5mTokens orders the results by the cache_creation_5m_tokens field. -func ByCacheCreation5mTokens(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldCacheCreation5mTokens, opts...).ToFunc() -} - -// ByCacheCreation1hTokens orders the results by the cache_creation_1h_tokens field. -func ByCacheCreation1hTokens(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldCacheCreation1hTokens, opts...).ToFunc() -} - // ByReasoningOutputTokens orders the results by the reasoning_output_tokens field. func ByReasoningOutputTokens(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldReasoningOutputTokens, opts...).ToFunc() @@ -356,11 +317,6 @@ func ByCacheCreationPrice(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCacheCreationPrice, opts...).ToFunc() } -// ByCacheCreation1hPrice orders the results by the cache_creation_1h_price field. -func ByCacheCreation1hPrice(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldCacheCreation1hPrice, opts...).ToFunc() -} - // ByInputCost orders the results by the input_cost field. func ByInputCost(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldInputCost, opts...).ToFunc() @@ -421,11 +377,6 @@ func ByServiceTier(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldServiceTier, opts...).ToFunc() } -// ByImageSize orders the results by the image_size field. -func ByImageSize(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldImageSize, opts...).ToFunc() -} - // ByStream orders the results by the stream field. func ByStream(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldStream, opts...).ToFunc() diff --git a/backend/ent/usagelog/where.go b/backend/ent/usagelog/where.go index c95d8710..4add6237 100644 --- a/backend/ent/usagelog/where.go +++ b/backend/ent/usagelog/where.go @@ -85,16 +85,6 @@ func CacheCreationTokens(v int) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldCacheCreationTokens, v)) } -// CacheCreation5mTokens applies equality check predicate on the "cache_creation_5m_tokens" field. It's identical to CacheCreation5mTokensEQ. -func CacheCreation5mTokens(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEQ(FieldCacheCreation5mTokens, v)) -} - -// CacheCreation1hTokens applies equality check predicate on the "cache_creation_1h_tokens" field. It's identical to CacheCreation1hTokensEQ. -func CacheCreation1hTokens(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEQ(FieldCacheCreation1hTokens, v)) -} - // ReasoningOutputTokens applies equality check predicate on the "reasoning_output_tokens" field. It's identical to ReasoningOutputTokensEQ. func ReasoningOutputTokens(v int) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldReasoningOutputTokens, v)) @@ -120,11 +110,6 @@ func CacheCreationPrice(v float64) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldCacheCreationPrice, v)) } -// CacheCreation1hPrice applies equality check predicate on the "cache_creation_1h_price" field. It's identical to CacheCreation1hPriceEQ. -func CacheCreation1hPrice(v float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEQ(FieldCacheCreation1hPrice, v)) -} - // InputCost applies equality check predicate on the "input_cost" field. It's identical to InputCostEQ. func InputCost(v float64) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldInputCost, v)) @@ -185,11 +170,6 @@ func ServiceTier(v string) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldServiceTier, v)) } -// ImageSize applies equality check predicate on the "image_size" field. It's identical to ImageSizeEQ. -func ImageSize(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v)) -} - // Stream applies equality check predicate on the "stream" field. It's identical to StreamEQ. func Stream(v bool) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldStream, v)) @@ -530,86 +510,6 @@ func CacheCreationTokensLTE(v int) predicate.UsageLog { return predicate.UsageLog(sql.FieldLTE(FieldCacheCreationTokens, v)) } -// CacheCreation5mTokensEQ applies the EQ predicate on the "cache_creation_5m_tokens" field. -func CacheCreation5mTokensEQ(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEQ(FieldCacheCreation5mTokens, v)) -} - -// CacheCreation5mTokensNEQ applies the NEQ predicate on the "cache_creation_5m_tokens" field. -func CacheCreation5mTokensNEQ(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldNEQ(FieldCacheCreation5mTokens, v)) -} - -// CacheCreation5mTokensIn applies the In predicate on the "cache_creation_5m_tokens" field. -func CacheCreation5mTokensIn(vs ...int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldIn(FieldCacheCreation5mTokens, vs...)) -} - -// CacheCreation5mTokensNotIn applies the NotIn predicate on the "cache_creation_5m_tokens" field. -func CacheCreation5mTokensNotIn(vs ...int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldNotIn(FieldCacheCreation5mTokens, vs...)) -} - -// CacheCreation5mTokensGT applies the GT predicate on the "cache_creation_5m_tokens" field. -func CacheCreation5mTokensGT(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldGT(FieldCacheCreation5mTokens, v)) -} - -// CacheCreation5mTokensGTE applies the GTE predicate on the "cache_creation_5m_tokens" field. -func CacheCreation5mTokensGTE(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldGTE(FieldCacheCreation5mTokens, v)) -} - -// CacheCreation5mTokensLT applies the LT predicate on the "cache_creation_5m_tokens" field. -func CacheCreation5mTokensLT(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldLT(FieldCacheCreation5mTokens, v)) -} - -// CacheCreation5mTokensLTE applies the LTE predicate on the "cache_creation_5m_tokens" field. -func CacheCreation5mTokensLTE(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldLTE(FieldCacheCreation5mTokens, v)) -} - -// CacheCreation1hTokensEQ applies the EQ predicate on the "cache_creation_1h_tokens" field. -func CacheCreation1hTokensEQ(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEQ(FieldCacheCreation1hTokens, v)) -} - -// CacheCreation1hTokensNEQ applies the NEQ predicate on the "cache_creation_1h_tokens" field. -func CacheCreation1hTokensNEQ(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldNEQ(FieldCacheCreation1hTokens, v)) -} - -// CacheCreation1hTokensIn applies the In predicate on the "cache_creation_1h_tokens" field. -func CacheCreation1hTokensIn(vs ...int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldIn(FieldCacheCreation1hTokens, vs...)) -} - -// CacheCreation1hTokensNotIn applies the NotIn predicate on the "cache_creation_1h_tokens" field. -func CacheCreation1hTokensNotIn(vs ...int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldNotIn(FieldCacheCreation1hTokens, vs...)) -} - -// CacheCreation1hTokensGT applies the GT predicate on the "cache_creation_1h_tokens" field. -func CacheCreation1hTokensGT(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldGT(FieldCacheCreation1hTokens, v)) -} - -// CacheCreation1hTokensGTE applies the GTE predicate on the "cache_creation_1h_tokens" field. -func CacheCreation1hTokensGTE(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldGTE(FieldCacheCreation1hTokens, v)) -} - -// CacheCreation1hTokensLT applies the LT predicate on the "cache_creation_1h_tokens" field. -func CacheCreation1hTokensLT(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldLT(FieldCacheCreation1hTokens, v)) -} - -// CacheCreation1hTokensLTE applies the LTE predicate on the "cache_creation_1h_tokens" field. -func CacheCreation1hTokensLTE(v int) predicate.UsageLog { - return predicate.UsageLog(sql.FieldLTE(FieldCacheCreation1hTokens, v)) -} - // ReasoningOutputTokensEQ applies the EQ predicate on the "reasoning_output_tokens" field. func ReasoningOutputTokensEQ(v int) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldReasoningOutputTokens, v)) @@ -810,46 +710,6 @@ func CacheCreationPriceLTE(v float64) predicate.UsageLog { return predicate.UsageLog(sql.FieldLTE(FieldCacheCreationPrice, v)) } -// CacheCreation1hPriceEQ applies the EQ predicate on the "cache_creation_1h_price" field. -func CacheCreation1hPriceEQ(v float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEQ(FieldCacheCreation1hPrice, v)) -} - -// CacheCreation1hPriceNEQ applies the NEQ predicate on the "cache_creation_1h_price" field. -func CacheCreation1hPriceNEQ(v float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldNEQ(FieldCacheCreation1hPrice, v)) -} - -// CacheCreation1hPriceIn applies the In predicate on the "cache_creation_1h_price" field. -func CacheCreation1hPriceIn(vs ...float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldIn(FieldCacheCreation1hPrice, vs...)) -} - -// CacheCreation1hPriceNotIn applies the NotIn predicate on the "cache_creation_1h_price" field. -func CacheCreation1hPriceNotIn(vs ...float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldNotIn(FieldCacheCreation1hPrice, vs...)) -} - -// CacheCreation1hPriceGT applies the GT predicate on the "cache_creation_1h_price" field. -func CacheCreation1hPriceGT(v float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldGT(FieldCacheCreation1hPrice, v)) -} - -// CacheCreation1hPriceGTE applies the GTE predicate on the "cache_creation_1h_price" field. -func CacheCreation1hPriceGTE(v float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldGTE(FieldCacheCreation1hPrice, v)) -} - -// CacheCreation1hPriceLT applies the LT predicate on the "cache_creation_1h_price" field. -func CacheCreation1hPriceLT(v float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldLT(FieldCacheCreation1hPrice, v)) -} - -// CacheCreation1hPriceLTE applies the LTE predicate on the "cache_creation_1h_price" field. -func CacheCreation1hPriceLTE(v float64) predicate.UsageLog { - return predicate.UsageLog(sql.FieldLTE(FieldCacheCreation1hPrice, v)) -} - // InputCostEQ applies the EQ predicate on the "input_cost" field. func InputCostEQ(v float64) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldInputCost, v)) @@ -1355,71 +1215,6 @@ func ServiceTierContainsFold(v string) predicate.UsageLog { return predicate.UsageLog(sql.FieldContainsFold(FieldServiceTier, v)) } -// ImageSizeEQ applies the EQ predicate on the "image_size" field. -func ImageSizeEQ(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v)) -} - -// ImageSizeNEQ applies the NEQ predicate on the "image_size" field. -func ImageSizeNEQ(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldNEQ(FieldImageSize, v)) -} - -// ImageSizeIn applies the In predicate on the "image_size" field. -func ImageSizeIn(vs ...string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldIn(FieldImageSize, vs...)) -} - -// ImageSizeNotIn applies the NotIn predicate on the "image_size" field. -func ImageSizeNotIn(vs ...string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldNotIn(FieldImageSize, vs...)) -} - -// ImageSizeGT applies the GT predicate on the "image_size" field. -func ImageSizeGT(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldGT(FieldImageSize, v)) -} - -// ImageSizeGTE applies the GTE predicate on the "image_size" field. -func ImageSizeGTE(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldGTE(FieldImageSize, v)) -} - -// ImageSizeLT applies the LT predicate on the "image_size" field. -func ImageSizeLT(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldLT(FieldImageSize, v)) -} - -// ImageSizeLTE applies the LTE predicate on the "image_size" field. -func ImageSizeLTE(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldLTE(FieldImageSize, v)) -} - -// ImageSizeContains applies the Contains predicate on the "image_size" field. -func ImageSizeContains(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldContains(FieldImageSize, v)) -} - -// ImageSizeHasPrefix applies the HasPrefix predicate on the "image_size" field. -func ImageSizeHasPrefix(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldHasPrefix(FieldImageSize, v)) -} - -// ImageSizeHasSuffix applies the HasSuffix predicate on the "image_size" field. -func ImageSizeHasSuffix(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldHasSuffix(FieldImageSize, v)) -} - -// ImageSizeEqualFold applies the EqualFold predicate on the "image_size" field. -func ImageSizeEqualFold(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldEqualFold(FieldImageSize, v)) -} - -// ImageSizeContainsFold applies the ContainsFold predicate on the "image_size" field. -func ImageSizeContainsFold(v string) predicate.UsageLog { - return predicate.UsageLog(sql.FieldContainsFold(FieldImageSize, v)) -} - // StreamEQ applies the EQ predicate on the "stream" field. func StreamEQ(v bool) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldStream, v)) @@ -1770,36 +1565,6 @@ func ReasoningEffortContainsFold(v string) predicate.UsageLog { return predicate.UsageLog(sql.FieldContainsFold(FieldReasoningEffort, v)) } -// UsageAttributesIsNil applies the IsNil predicate on the "usage_attributes" field. -func UsageAttributesIsNil() predicate.UsageLog { - return predicate.UsageLog(sql.FieldIsNull(FieldUsageAttributes)) -} - -// UsageAttributesNotNil applies the NotNil predicate on the "usage_attributes" field. -func UsageAttributesNotNil() predicate.UsageLog { - return predicate.UsageLog(sql.FieldNotNull(FieldUsageAttributes)) -} - -// UsageMetricsIsNil applies the IsNil predicate on the "usage_metrics" field. -func UsageMetricsIsNil() predicate.UsageLog { - return predicate.UsageLog(sql.FieldIsNull(FieldUsageMetrics)) -} - -// UsageMetricsNotNil applies the NotNil predicate on the "usage_metrics" field. -func UsageMetricsNotNil() predicate.UsageLog { - return predicate.UsageLog(sql.FieldNotNull(FieldUsageMetrics)) -} - -// UsageCostDetailsIsNil applies the IsNil predicate on the "usage_cost_details" field. -func UsageCostDetailsIsNil() predicate.UsageLog { - return predicate.UsageLog(sql.FieldIsNull(FieldUsageCostDetails)) -} - -// UsageCostDetailsNotNil applies the NotNil predicate on the "usage_cost_details" field. -func UsageCostDetailsNotNil() predicate.UsageLog { - return predicate.UsageLog(sql.FieldNotNull(FieldUsageCostDetails)) -} - // UsageMetadataIsNil applies the IsNil predicate on the "usage_metadata" field. func UsageMetadataIsNil() predicate.UsageLog { return predicate.UsageLog(sql.FieldIsNull(FieldUsageMetadata)) diff --git a/backend/ent/usagelog_create.go b/backend/ent/usagelog_create.go index 260a75e5..31ffc208 100644 --- a/backend/ent/usagelog_create.go +++ b/backend/ent/usagelog_create.go @@ -15,7 +15,6 @@ import ( "github.com/DouDOU-start/airgate-core/ent/group" "github.com/DouDOU-start/airgate-core/ent/usagelog" "github.com/DouDOU-start/airgate-core/ent/user" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // UsageLogCreate is the builder for creating a UsageLog entity. @@ -93,34 +92,6 @@ func (ulc *UsageLogCreate) SetNillableCacheCreationTokens(i *int) *UsageLogCreat return ulc } -// SetCacheCreation5mTokens sets the "cache_creation_5m_tokens" field. -func (ulc *UsageLogCreate) SetCacheCreation5mTokens(i int) *UsageLogCreate { - ulc.mutation.SetCacheCreation5mTokens(i) - return ulc -} - -// SetNillableCacheCreation5mTokens sets the "cache_creation_5m_tokens" field if the given value is not nil. -func (ulc *UsageLogCreate) SetNillableCacheCreation5mTokens(i *int) *UsageLogCreate { - if i != nil { - ulc.SetCacheCreation5mTokens(*i) - } - return ulc -} - -// SetCacheCreation1hTokens sets the "cache_creation_1h_tokens" field. -func (ulc *UsageLogCreate) SetCacheCreation1hTokens(i int) *UsageLogCreate { - ulc.mutation.SetCacheCreation1hTokens(i) - return ulc -} - -// SetNillableCacheCreation1hTokens sets the "cache_creation_1h_tokens" field if the given value is not nil. -func (ulc *UsageLogCreate) SetNillableCacheCreation1hTokens(i *int) *UsageLogCreate { - if i != nil { - ulc.SetCacheCreation1hTokens(*i) - } - return ulc -} - // SetReasoningOutputTokens sets the "reasoning_output_tokens" field. func (ulc *UsageLogCreate) SetReasoningOutputTokens(i int) *UsageLogCreate { ulc.mutation.SetReasoningOutputTokens(i) @@ -191,20 +162,6 @@ func (ulc *UsageLogCreate) SetNillableCacheCreationPrice(f *float64) *UsageLogCr return ulc } -// SetCacheCreation1hPrice sets the "cache_creation_1h_price" field. -func (ulc *UsageLogCreate) SetCacheCreation1hPrice(f float64) *UsageLogCreate { - ulc.mutation.SetCacheCreation1hPrice(f) - return ulc -} - -// SetNillableCacheCreation1hPrice sets the "cache_creation_1h_price" field if the given value is not nil. -func (ulc *UsageLogCreate) SetNillableCacheCreation1hPrice(f *float64) *UsageLogCreate { - if f != nil { - ulc.SetCacheCreation1hPrice(*f) - } - return ulc -} - // SetInputCost sets the "input_cost" field. func (ulc *UsageLogCreate) SetInputCost(f float64) *UsageLogCreate { ulc.mutation.SetInputCost(f) @@ -373,20 +330,6 @@ func (ulc *UsageLogCreate) SetNillableServiceTier(s *string) *UsageLogCreate { return ulc } -// SetImageSize sets the "image_size" field. -func (ulc *UsageLogCreate) SetImageSize(s string) *UsageLogCreate { - ulc.mutation.SetImageSize(s) - return ulc -} - -// SetNillableImageSize sets the "image_size" field if the given value is not nil. -func (ulc *UsageLogCreate) SetNillableImageSize(s *string) *UsageLogCreate { - if s != nil { - ulc.SetImageSize(*s) - } - return ulc -} - // SetStream sets the "stream" field. func (ulc *UsageLogCreate) SetStream(b bool) *UsageLogCreate { ulc.mutation.SetStream(b) @@ -485,24 +428,6 @@ func (ulc *UsageLogCreate) SetNillableReasoningEffort(s *string) *UsageLogCreate return ulc } -// SetUsageAttributes sets the "usage_attributes" field. -func (ulc *UsageLogCreate) SetUsageAttributes(sa []sdk.UsageAttribute) *UsageLogCreate { - ulc.mutation.SetUsageAttributes(sa) - return ulc -} - -// SetUsageMetrics sets the "usage_metrics" field. -func (ulc *UsageLogCreate) SetUsageMetrics(sm []sdk.UsageMetric) *UsageLogCreate { - ulc.mutation.SetUsageMetrics(sm) - return ulc -} - -// SetUsageCostDetails sets the "usage_cost_details" field. -func (ulc *UsageLogCreate) SetUsageCostDetails(scd []sdk.UsageCostDetail) *UsageLogCreate { - ulc.mutation.SetUsageCostDetails(scd) - return ulc -} - // SetUsageMetadata sets the "usage_metadata" field. func (ulc *UsageLogCreate) SetUsageMetadata(m map[string]string) *UsageLogCreate { ulc.mutation.SetUsageMetadata(m) @@ -678,14 +603,6 @@ func (ulc *UsageLogCreate) defaults() { v := usagelog.DefaultCacheCreationTokens ulc.mutation.SetCacheCreationTokens(v) } - if _, ok := ulc.mutation.CacheCreation5mTokens(); !ok { - v := usagelog.DefaultCacheCreation5mTokens - ulc.mutation.SetCacheCreation5mTokens(v) - } - if _, ok := ulc.mutation.CacheCreation1hTokens(); !ok { - v := usagelog.DefaultCacheCreation1hTokens - ulc.mutation.SetCacheCreation1hTokens(v) - } if _, ok := ulc.mutation.ReasoningOutputTokens(); !ok { v := usagelog.DefaultReasoningOutputTokens ulc.mutation.SetReasoningOutputTokens(v) @@ -706,10 +623,6 @@ func (ulc *UsageLogCreate) defaults() { v := usagelog.DefaultCacheCreationPrice ulc.mutation.SetCacheCreationPrice(v) } - if _, ok := ulc.mutation.CacheCreation1hPrice(); !ok { - v := usagelog.DefaultCacheCreation1hPrice - ulc.mutation.SetCacheCreation1hPrice(v) - } if _, ok := ulc.mutation.InputCost(); !ok { v := usagelog.DefaultInputCost ulc.mutation.SetInputCost(v) @@ -758,10 +671,6 @@ func (ulc *UsageLogCreate) defaults() { v := usagelog.DefaultServiceTier ulc.mutation.SetServiceTier(v) } - if _, ok := ulc.mutation.ImageSize(); !ok { - v := usagelog.DefaultImageSize - ulc.mutation.SetImageSize(v) - } if _, ok := ulc.mutation.Stream(); !ok { v := usagelog.DefaultStream ulc.mutation.SetStream(v) @@ -834,12 +743,6 @@ func (ulc *UsageLogCreate) check() error { if _, ok := ulc.mutation.CacheCreationTokens(); !ok { return &ValidationError{Name: "cache_creation_tokens", err: errors.New(`ent: missing required field "UsageLog.cache_creation_tokens"`)} } - if _, ok := ulc.mutation.CacheCreation5mTokens(); !ok { - return &ValidationError{Name: "cache_creation_5m_tokens", err: errors.New(`ent: missing required field "UsageLog.cache_creation_5m_tokens"`)} - } - if _, ok := ulc.mutation.CacheCreation1hTokens(); !ok { - return &ValidationError{Name: "cache_creation_1h_tokens", err: errors.New(`ent: missing required field "UsageLog.cache_creation_1h_tokens"`)} - } if _, ok := ulc.mutation.ReasoningOutputTokens(); !ok { return &ValidationError{Name: "reasoning_output_tokens", err: errors.New(`ent: missing required field "UsageLog.reasoning_output_tokens"`)} } @@ -855,9 +758,6 @@ func (ulc *UsageLogCreate) check() error { if _, ok := ulc.mutation.CacheCreationPrice(); !ok { return &ValidationError{Name: "cache_creation_price", err: errors.New(`ent: missing required field "UsageLog.cache_creation_price"`)} } - if _, ok := ulc.mutation.CacheCreation1hPrice(); !ok { - return &ValidationError{Name: "cache_creation_1h_price", err: errors.New(`ent: missing required field "UsageLog.cache_creation_1h_price"`)} - } if _, ok := ulc.mutation.InputCost(); !ok { return &ValidationError{Name: "input_cost", err: errors.New(`ent: missing required field "UsageLog.input_cost"`)} } @@ -894,9 +794,6 @@ func (ulc *UsageLogCreate) check() error { if _, ok := ulc.mutation.ServiceTier(); !ok { return &ValidationError{Name: "service_tier", err: errors.New(`ent: missing required field "UsageLog.service_tier"`)} } - if _, ok := ulc.mutation.ImageSize(); !ok { - return &ValidationError{Name: "image_size", err: errors.New(`ent: missing required field "UsageLog.image_size"`)} - } if _, ok := ulc.mutation.Stream(); !ok { return &ValidationError{Name: "stream", err: errors.New(`ent: missing required field "UsageLog.stream"`)} } @@ -977,14 +874,6 @@ func (ulc *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) { _spec.SetField(usagelog.FieldCacheCreationTokens, field.TypeInt, value) _node.CacheCreationTokens = value } - if value, ok := ulc.mutation.CacheCreation5mTokens(); ok { - _spec.SetField(usagelog.FieldCacheCreation5mTokens, field.TypeInt, value) - _node.CacheCreation5mTokens = value - } - if value, ok := ulc.mutation.CacheCreation1hTokens(); ok { - _spec.SetField(usagelog.FieldCacheCreation1hTokens, field.TypeInt, value) - _node.CacheCreation1hTokens = value - } if value, ok := ulc.mutation.ReasoningOutputTokens(); ok { _spec.SetField(usagelog.FieldReasoningOutputTokens, field.TypeInt, value) _node.ReasoningOutputTokens = value @@ -1005,10 +894,6 @@ func (ulc *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) { _spec.SetField(usagelog.FieldCacheCreationPrice, field.TypeFloat64, value) _node.CacheCreationPrice = value } - if value, ok := ulc.mutation.CacheCreation1hPrice(); ok { - _spec.SetField(usagelog.FieldCacheCreation1hPrice, field.TypeFloat64, value) - _node.CacheCreation1hPrice = value - } if value, ok := ulc.mutation.InputCost(); ok { _spec.SetField(usagelog.FieldInputCost, field.TypeFloat64, value) _node.InputCost = value @@ -1057,10 +942,6 @@ func (ulc *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) { _spec.SetField(usagelog.FieldServiceTier, field.TypeString, value) _node.ServiceTier = value } - if value, ok := ulc.mutation.ImageSize(); ok { - _spec.SetField(usagelog.FieldImageSize, field.TypeString, value) - _node.ImageSize = value - } if value, ok := ulc.mutation.Stream(); ok { _spec.SetField(usagelog.FieldStream, field.TypeBool, value) _node.Stream = value @@ -1089,18 +970,6 @@ func (ulc *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) { _spec.SetField(usagelog.FieldReasoningEffort, field.TypeString, value) _node.ReasoningEffort = value } - if value, ok := ulc.mutation.UsageAttributes(); ok { - _spec.SetField(usagelog.FieldUsageAttributes, field.TypeJSON, value) - _node.UsageAttributes = value - } - if value, ok := ulc.mutation.UsageMetrics(); ok { - _spec.SetField(usagelog.FieldUsageMetrics, field.TypeJSON, value) - _node.UsageMetrics = value - } - if value, ok := ulc.mutation.UsageCostDetails(); ok { - _spec.SetField(usagelog.FieldUsageCostDetails, field.TypeJSON, value) - _node.UsageCostDetails = value - } if value, ok := ulc.mutation.UsageMetadata(); ok { _spec.SetField(usagelog.FieldUsageMetadata, field.TypeJSON, value) _node.UsageMetadata = value diff --git a/backend/ent/usagelog_update.go b/backend/ent/usagelog_update.go index c20a4b01..e5bc1832 100644 --- a/backend/ent/usagelog_update.go +++ b/backend/ent/usagelog_update.go @@ -9,7 +9,6 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" - "entgo.io/ent/dialect/sql/sqljson" "entgo.io/ent/schema/field" "github.com/DouDOU-start/airgate-core/ent/account" "github.com/DouDOU-start/airgate-core/ent/apikey" @@ -17,7 +16,6 @@ import ( "github.com/DouDOU-start/airgate-core/ent/predicate" "github.com/DouDOU-start/airgate-core/ent/usagelog" "github.com/DouDOU-start/airgate-core/ent/user" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" ) // UsageLogUpdate is the builder for updating UsageLog entities. @@ -145,48 +143,6 @@ func (ulu *UsageLogUpdate) AddCacheCreationTokens(i int) *UsageLogUpdate { return ulu } -// SetCacheCreation5mTokens sets the "cache_creation_5m_tokens" field. -func (ulu *UsageLogUpdate) SetCacheCreation5mTokens(i int) *UsageLogUpdate { - ulu.mutation.ResetCacheCreation5mTokens() - ulu.mutation.SetCacheCreation5mTokens(i) - return ulu -} - -// SetNillableCacheCreation5mTokens sets the "cache_creation_5m_tokens" field if the given value is not nil. -func (ulu *UsageLogUpdate) SetNillableCacheCreation5mTokens(i *int) *UsageLogUpdate { - if i != nil { - ulu.SetCacheCreation5mTokens(*i) - } - return ulu -} - -// AddCacheCreation5mTokens adds i to the "cache_creation_5m_tokens" field. -func (ulu *UsageLogUpdate) AddCacheCreation5mTokens(i int) *UsageLogUpdate { - ulu.mutation.AddCacheCreation5mTokens(i) - return ulu -} - -// SetCacheCreation1hTokens sets the "cache_creation_1h_tokens" field. -func (ulu *UsageLogUpdate) SetCacheCreation1hTokens(i int) *UsageLogUpdate { - ulu.mutation.ResetCacheCreation1hTokens() - ulu.mutation.SetCacheCreation1hTokens(i) - return ulu -} - -// SetNillableCacheCreation1hTokens sets the "cache_creation_1h_tokens" field if the given value is not nil. -func (ulu *UsageLogUpdate) SetNillableCacheCreation1hTokens(i *int) *UsageLogUpdate { - if i != nil { - ulu.SetCacheCreation1hTokens(*i) - } - return ulu -} - -// AddCacheCreation1hTokens adds i to the "cache_creation_1h_tokens" field. -func (ulu *UsageLogUpdate) AddCacheCreation1hTokens(i int) *UsageLogUpdate { - ulu.mutation.AddCacheCreation1hTokens(i) - return ulu -} - // SetReasoningOutputTokens sets the "reasoning_output_tokens" field. func (ulu *UsageLogUpdate) SetReasoningOutputTokens(i int) *UsageLogUpdate { ulu.mutation.ResetReasoningOutputTokens() @@ -292,27 +248,6 @@ func (ulu *UsageLogUpdate) AddCacheCreationPrice(f float64) *UsageLogUpdate { return ulu } -// SetCacheCreation1hPrice sets the "cache_creation_1h_price" field. -func (ulu *UsageLogUpdate) SetCacheCreation1hPrice(f float64) *UsageLogUpdate { - ulu.mutation.ResetCacheCreation1hPrice() - ulu.mutation.SetCacheCreation1hPrice(f) - return ulu -} - -// SetNillableCacheCreation1hPrice sets the "cache_creation_1h_price" field if the given value is not nil. -func (ulu *UsageLogUpdate) SetNillableCacheCreation1hPrice(f *float64) *UsageLogUpdate { - if f != nil { - ulu.SetCacheCreation1hPrice(*f) - } - return ulu -} - -// AddCacheCreation1hPrice adds f to the "cache_creation_1h_price" field. -func (ulu *UsageLogUpdate) AddCacheCreation1hPrice(f float64) *UsageLogUpdate { - ulu.mutation.AddCacheCreation1hPrice(f) - return ulu -} - // SetInputCost sets the "input_cost" field. func (ulu *UsageLogUpdate) SetInputCost(f float64) *UsageLogUpdate { ulu.mutation.ResetInputCost() @@ -558,20 +493,6 @@ func (ulu *UsageLogUpdate) SetNillableServiceTier(s *string) *UsageLogUpdate { return ulu } -// SetImageSize sets the "image_size" field. -func (ulu *UsageLogUpdate) SetImageSize(s string) *UsageLogUpdate { - ulu.mutation.SetImageSize(s) - return ulu -} - -// SetNillableImageSize sets the "image_size" field if the given value is not nil. -func (ulu *UsageLogUpdate) SetNillableImageSize(s *string) *UsageLogUpdate { - if s != nil { - ulu.SetImageSize(*s) - } - return ulu -} - // SetStream sets the "stream" field. func (ulu *UsageLogUpdate) SetStream(b bool) *UsageLogUpdate { ulu.mutation.SetStream(b) @@ -684,60 +605,6 @@ func (ulu *UsageLogUpdate) SetNillableReasoningEffort(s *string) *UsageLogUpdate return ulu } -// SetUsageAttributes sets the "usage_attributes" field. -func (ulu *UsageLogUpdate) SetUsageAttributes(sa []sdk.UsageAttribute) *UsageLogUpdate { - ulu.mutation.SetUsageAttributes(sa) - return ulu -} - -// AppendUsageAttributes appends sa to the "usage_attributes" field. -func (ulu *UsageLogUpdate) AppendUsageAttributes(sa []sdk.UsageAttribute) *UsageLogUpdate { - ulu.mutation.AppendUsageAttributes(sa) - return ulu -} - -// ClearUsageAttributes clears the value of the "usage_attributes" field. -func (ulu *UsageLogUpdate) ClearUsageAttributes() *UsageLogUpdate { - ulu.mutation.ClearUsageAttributes() - return ulu -} - -// SetUsageMetrics sets the "usage_metrics" field. -func (ulu *UsageLogUpdate) SetUsageMetrics(sm []sdk.UsageMetric) *UsageLogUpdate { - ulu.mutation.SetUsageMetrics(sm) - return ulu -} - -// AppendUsageMetrics appends sm to the "usage_metrics" field. -func (ulu *UsageLogUpdate) AppendUsageMetrics(sm []sdk.UsageMetric) *UsageLogUpdate { - ulu.mutation.AppendUsageMetrics(sm) - return ulu -} - -// ClearUsageMetrics clears the value of the "usage_metrics" field. -func (ulu *UsageLogUpdate) ClearUsageMetrics() *UsageLogUpdate { - ulu.mutation.ClearUsageMetrics() - return ulu -} - -// SetUsageCostDetails sets the "usage_cost_details" field. -func (ulu *UsageLogUpdate) SetUsageCostDetails(scd []sdk.UsageCostDetail) *UsageLogUpdate { - ulu.mutation.SetUsageCostDetails(scd) - return ulu -} - -// AppendUsageCostDetails appends scd to the "usage_cost_details" field. -func (ulu *UsageLogUpdate) AppendUsageCostDetails(scd []sdk.UsageCostDetail) *UsageLogUpdate { - ulu.mutation.AppendUsageCostDetails(scd) - return ulu -} - -// ClearUsageCostDetails clears the value of the "usage_cost_details" field. -func (ulu *UsageLogUpdate) ClearUsageCostDetails() *UsageLogUpdate { - ulu.mutation.ClearUsageCostDetails() - return ulu -} - // SetUsageMetadata sets the "usage_metadata" field. func (ulu *UsageLogUpdate) SetUsageMetadata(m map[string]string) *UsageLogUpdate { ulu.mutation.SetUsageMetadata(m) @@ -974,18 +841,6 @@ func (ulu *UsageLogUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := ulu.mutation.AddedCacheCreationTokens(); ok { _spec.AddField(usagelog.FieldCacheCreationTokens, field.TypeInt, value) } - if value, ok := ulu.mutation.CacheCreation5mTokens(); ok { - _spec.SetField(usagelog.FieldCacheCreation5mTokens, field.TypeInt, value) - } - if value, ok := ulu.mutation.AddedCacheCreation5mTokens(); ok { - _spec.AddField(usagelog.FieldCacheCreation5mTokens, field.TypeInt, value) - } - if value, ok := ulu.mutation.CacheCreation1hTokens(); ok { - _spec.SetField(usagelog.FieldCacheCreation1hTokens, field.TypeInt, value) - } - if value, ok := ulu.mutation.AddedCacheCreation1hTokens(); ok { - _spec.AddField(usagelog.FieldCacheCreation1hTokens, field.TypeInt, value) - } if value, ok := ulu.mutation.ReasoningOutputTokens(); ok { _spec.SetField(usagelog.FieldReasoningOutputTokens, field.TypeInt, value) } @@ -1016,12 +871,6 @@ func (ulu *UsageLogUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := ulu.mutation.AddedCacheCreationPrice(); ok { _spec.AddField(usagelog.FieldCacheCreationPrice, field.TypeFloat64, value) } - if value, ok := ulu.mutation.CacheCreation1hPrice(); ok { - _spec.SetField(usagelog.FieldCacheCreation1hPrice, field.TypeFloat64, value) - } - if value, ok := ulu.mutation.AddedCacheCreation1hPrice(); ok { - _spec.AddField(usagelog.FieldCacheCreation1hPrice, field.TypeFloat64, value) - } if value, ok := ulu.mutation.InputCost(); ok { _spec.SetField(usagelog.FieldInputCost, field.TypeFloat64, value) } @@ -1091,9 +940,6 @@ func (ulu *UsageLogUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := ulu.mutation.ServiceTier(); ok { _spec.SetField(usagelog.FieldServiceTier, field.TypeString, value) } - if value, ok := ulu.mutation.ImageSize(); ok { - _spec.SetField(usagelog.FieldImageSize, field.TypeString, value) - } if value, ok := ulu.mutation.Stream(); ok { _spec.SetField(usagelog.FieldStream, field.TypeBool, value) } @@ -1121,39 +967,6 @@ func (ulu *UsageLogUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := ulu.mutation.ReasoningEffort(); ok { _spec.SetField(usagelog.FieldReasoningEffort, field.TypeString, value) } - if value, ok := ulu.mutation.UsageAttributes(); ok { - _spec.SetField(usagelog.FieldUsageAttributes, field.TypeJSON, value) - } - if value, ok := ulu.mutation.AppendedUsageAttributes(); ok { - _spec.AddModifier(func(u *sql.UpdateBuilder) { - sqljson.Append(u, usagelog.FieldUsageAttributes, value) - }) - } - if ulu.mutation.UsageAttributesCleared() { - _spec.ClearField(usagelog.FieldUsageAttributes, field.TypeJSON) - } - if value, ok := ulu.mutation.UsageMetrics(); ok { - _spec.SetField(usagelog.FieldUsageMetrics, field.TypeJSON, value) - } - if value, ok := ulu.mutation.AppendedUsageMetrics(); ok { - _spec.AddModifier(func(u *sql.UpdateBuilder) { - sqljson.Append(u, usagelog.FieldUsageMetrics, value) - }) - } - if ulu.mutation.UsageMetricsCleared() { - _spec.ClearField(usagelog.FieldUsageMetrics, field.TypeJSON) - } - if value, ok := ulu.mutation.UsageCostDetails(); ok { - _spec.SetField(usagelog.FieldUsageCostDetails, field.TypeJSON, value) - } - if value, ok := ulu.mutation.AppendedUsageCostDetails(); ok { - _spec.AddModifier(func(u *sql.UpdateBuilder) { - sqljson.Append(u, usagelog.FieldUsageCostDetails, value) - }) - } - if ulu.mutation.UsageCostDetailsCleared() { - _spec.ClearField(usagelog.FieldUsageCostDetails, field.TypeJSON) - } if value, ok := ulu.mutation.UsageMetadata(); ok { _spec.SetField(usagelog.FieldUsageMetadata, field.TypeJSON, value) } @@ -1417,48 +1230,6 @@ func (uluo *UsageLogUpdateOne) AddCacheCreationTokens(i int) *UsageLogUpdateOne return uluo } -// SetCacheCreation5mTokens sets the "cache_creation_5m_tokens" field. -func (uluo *UsageLogUpdateOne) SetCacheCreation5mTokens(i int) *UsageLogUpdateOne { - uluo.mutation.ResetCacheCreation5mTokens() - uluo.mutation.SetCacheCreation5mTokens(i) - return uluo -} - -// SetNillableCacheCreation5mTokens sets the "cache_creation_5m_tokens" field if the given value is not nil. -func (uluo *UsageLogUpdateOne) SetNillableCacheCreation5mTokens(i *int) *UsageLogUpdateOne { - if i != nil { - uluo.SetCacheCreation5mTokens(*i) - } - return uluo -} - -// AddCacheCreation5mTokens adds i to the "cache_creation_5m_tokens" field. -func (uluo *UsageLogUpdateOne) AddCacheCreation5mTokens(i int) *UsageLogUpdateOne { - uluo.mutation.AddCacheCreation5mTokens(i) - return uluo -} - -// SetCacheCreation1hTokens sets the "cache_creation_1h_tokens" field. -func (uluo *UsageLogUpdateOne) SetCacheCreation1hTokens(i int) *UsageLogUpdateOne { - uluo.mutation.ResetCacheCreation1hTokens() - uluo.mutation.SetCacheCreation1hTokens(i) - return uluo -} - -// SetNillableCacheCreation1hTokens sets the "cache_creation_1h_tokens" field if the given value is not nil. -func (uluo *UsageLogUpdateOne) SetNillableCacheCreation1hTokens(i *int) *UsageLogUpdateOne { - if i != nil { - uluo.SetCacheCreation1hTokens(*i) - } - return uluo -} - -// AddCacheCreation1hTokens adds i to the "cache_creation_1h_tokens" field. -func (uluo *UsageLogUpdateOne) AddCacheCreation1hTokens(i int) *UsageLogUpdateOne { - uluo.mutation.AddCacheCreation1hTokens(i) - return uluo -} - // SetReasoningOutputTokens sets the "reasoning_output_tokens" field. func (uluo *UsageLogUpdateOne) SetReasoningOutputTokens(i int) *UsageLogUpdateOne { uluo.mutation.ResetReasoningOutputTokens() @@ -1564,27 +1335,6 @@ func (uluo *UsageLogUpdateOne) AddCacheCreationPrice(f float64) *UsageLogUpdateO return uluo } -// SetCacheCreation1hPrice sets the "cache_creation_1h_price" field. -func (uluo *UsageLogUpdateOne) SetCacheCreation1hPrice(f float64) *UsageLogUpdateOne { - uluo.mutation.ResetCacheCreation1hPrice() - uluo.mutation.SetCacheCreation1hPrice(f) - return uluo -} - -// SetNillableCacheCreation1hPrice sets the "cache_creation_1h_price" field if the given value is not nil. -func (uluo *UsageLogUpdateOne) SetNillableCacheCreation1hPrice(f *float64) *UsageLogUpdateOne { - if f != nil { - uluo.SetCacheCreation1hPrice(*f) - } - return uluo -} - -// AddCacheCreation1hPrice adds f to the "cache_creation_1h_price" field. -func (uluo *UsageLogUpdateOne) AddCacheCreation1hPrice(f float64) *UsageLogUpdateOne { - uluo.mutation.AddCacheCreation1hPrice(f) - return uluo -} - // SetInputCost sets the "input_cost" field. func (uluo *UsageLogUpdateOne) SetInputCost(f float64) *UsageLogUpdateOne { uluo.mutation.ResetInputCost() @@ -1830,20 +1580,6 @@ func (uluo *UsageLogUpdateOne) SetNillableServiceTier(s *string) *UsageLogUpdate return uluo } -// SetImageSize sets the "image_size" field. -func (uluo *UsageLogUpdateOne) SetImageSize(s string) *UsageLogUpdateOne { - uluo.mutation.SetImageSize(s) - return uluo -} - -// SetNillableImageSize sets the "image_size" field if the given value is not nil. -func (uluo *UsageLogUpdateOne) SetNillableImageSize(s *string) *UsageLogUpdateOne { - if s != nil { - uluo.SetImageSize(*s) - } - return uluo -} - // SetStream sets the "stream" field. func (uluo *UsageLogUpdateOne) SetStream(b bool) *UsageLogUpdateOne { uluo.mutation.SetStream(b) @@ -1956,60 +1692,6 @@ func (uluo *UsageLogUpdateOne) SetNillableReasoningEffort(s *string) *UsageLogUp return uluo } -// SetUsageAttributes sets the "usage_attributes" field. -func (uluo *UsageLogUpdateOne) SetUsageAttributes(sa []sdk.UsageAttribute) *UsageLogUpdateOne { - uluo.mutation.SetUsageAttributes(sa) - return uluo -} - -// AppendUsageAttributes appends sa to the "usage_attributes" field. -func (uluo *UsageLogUpdateOne) AppendUsageAttributes(sa []sdk.UsageAttribute) *UsageLogUpdateOne { - uluo.mutation.AppendUsageAttributes(sa) - return uluo -} - -// ClearUsageAttributes clears the value of the "usage_attributes" field. -func (uluo *UsageLogUpdateOne) ClearUsageAttributes() *UsageLogUpdateOne { - uluo.mutation.ClearUsageAttributes() - return uluo -} - -// SetUsageMetrics sets the "usage_metrics" field. -func (uluo *UsageLogUpdateOne) SetUsageMetrics(sm []sdk.UsageMetric) *UsageLogUpdateOne { - uluo.mutation.SetUsageMetrics(sm) - return uluo -} - -// AppendUsageMetrics appends sm to the "usage_metrics" field. -func (uluo *UsageLogUpdateOne) AppendUsageMetrics(sm []sdk.UsageMetric) *UsageLogUpdateOne { - uluo.mutation.AppendUsageMetrics(sm) - return uluo -} - -// ClearUsageMetrics clears the value of the "usage_metrics" field. -func (uluo *UsageLogUpdateOne) ClearUsageMetrics() *UsageLogUpdateOne { - uluo.mutation.ClearUsageMetrics() - return uluo -} - -// SetUsageCostDetails sets the "usage_cost_details" field. -func (uluo *UsageLogUpdateOne) SetUsageCostDetails(scd []sdk.UsageCostDetail) *UsageLogUpdateOne { - uluo.mutation.SetUsageCostDetails(scd) - return uluo -} - -// AppendUsageCostDetails appends scd to the "usage_cost_details" field. -func (uluo *UsageLogUpdateOne) AppendUsageCostDetails(scd []sdk.UsageCostDetail) *UsageLogUpdateOne { - uluo.mutation.AppendUsageCostDetails(scd) - return uluo -} - -// ClearUsageCostDetails clears the value of the "usage_cost_details" field. -func (uluo *UsageLogUpdateOne) ClearUsageCostDetails() *UsageLogUpdateOne { - uluo.mutation.ClearUsageCostDetails() - return uluo -} - // SetUsageMetadata sets the "usage_metadata" field. func (uluo *UsageLogUpdateOne) SetUsageMetadata(m map[string]string) *UsageLogUpdateOne { uluo.mutation.SetUsageMetadata(m) @@ -2276,18 +1958,6 @@ func (uluo *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, er if value, ok := uluo.mutation.AddedCacheCreationTokens(); ok { _spec.AddField(usagelog.FieldCacheCreationTokens, field.TypeInt, value) } - if value, ok := uluo.mutation.CacheCreation5mTokens(); ok { - _spec.SetField(usagelog.FieldCacheCreation5mTokens, field.TypeInt, value) - } - if value, ok := uluo.mutation.AddedCacheCreation5mTokens(); ok { - _spec.AddField(usagelog.FieldCacheCreation5mTokens, field.TypeInt, value) - } - if value, ok := uluo.mutation.CacheCreation1hTokens(); ok { - _spec.SetField(usagelog.FieldCacheCreation1hTokens, field.TypeInt, value) - } - if value, ok := uluo.mutation.AddedCacheCreation1hTokens(); ok { - _spec.AddField(usagelog.FieldCacheCreation1hTokens, field.TypeInt, value) - } if value, ok := uluo.mutation.ReasoningOutputTokens(); ok { _spec.SetField(usagelog.FieldReasoningOutputTokens, field.TypeInt, value) } @@ -2318,12 +1988,6 @@ func (uluo *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, er if value, ok := uluo.mutation.AddedCacheCreationPrice(); ok { _spec.AddField(usagelog.FieldCacheCreationPrice, field.TypeFloat64, value) } - if value, ok := uluo.mutation.CacheCreation1hPrice(); ok { - _spec.SetField(usagelog.FieldCacheCreation1hPrice, field.TypeFloat64, value) - } - if value, ok := uluo.mutation.AddedCacheCreation1hPrice(); ok { - _spec.AddField(usagelog.FieldCacheCreation1hPrice, field.TypeFloat64, value) - } if value, ok := uluo.mutation.InputCost(); ok { _spec.SetField(usagelog.FieldInputCost, field.TypeFloat64, value) } @@ -2393,9 +2057,6 @@ func (uluo *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, er if value, ok := uluo.mutation.ServiceTier(); ok { _spec.SetField(usagelog.FieldServiceTier, field.TypeString, value) } - if value, ok := uluo.mutation.ImageSize(); ok { - _spec.SetField(usagelog.FieldImageSize, field.TypeString, value) - } if value, ok := uluo.mutation.Stream(); ok { _spec.SetField(usagelog.FieldStream, field.TypeBool, value) } @@ -2423,39 +2084,6 @@ func (uluo *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, er if value, ok := uluo.mutation.ReasoningEffort(); ok { _spec.SetField(usagelog.FieldReasoningEffort, field.TypeString, value) } - if value, ok := uluo.mutation.UsageAttributes(); ok { - _spec.SetField(usagelog.FieldUsageAttributes, field.TypeJSON, value) - } - if value, ok := uluo.mutation.AppendedUsageAttributes(); ok { - _spec.AddModifier(func(u *sql.UpdateBuilder) { - sqljson.Append(u, usagelog.FieldUsageAttributes, value) - }) - } - if uluo.mutation.UsageAttributesCleared() { - _spec.ClearField(usagelog.FieldUsageAttributes, field.TypeJSON) - } - if value, ok := uluo.mutation.UsageMetrics(); ok { - _spec.SetField(usagelog.FieldUsageMetrics, field.TypeJSON, value) - } - if value, ok := uluo.mutation.AppendedUsageMetrics(); ok { - _spec.AddModifier(func(u *sql.UpdateBuilder) { - sqljson.Append(u, usagelog.FieldUsageMetrics, value) - }) - } - if uluo.mutation.UsageMetricsCleared() { - _spec.ClearField(usagelog.FieldUsageMetrics, field.TypeJSON) - } - if value, ok := uluo.mutation.UsageCostDetails(); ok { - _spec.SetField(usagelog.FieldUsageCostDetails, field.TypeJSON, value) - } - if value, ok := uluo.mutation.AppendedUsageCostDetails(); ok { - _spec.AddModifier(func(u *sql.UpdateBuilder) { - sqljson.Append(u, usagelog.FieldUsageCostDetails, value) - }) - } - if uluo.mutation.UsageCostDetailsCleared() { - _spec.ClearField(usagelog.FieldUsageCostDetails, field.TypeJSON) - } if value, ok := uluo.mutation.UsageMetadata(); ok { _spec.SetField(usagelog.FieldUsageMetadata, field.TypeJSON, value) } diff --git a/backend/internal/app/usage/types.go b/backend/internal/app/usage/types.go index c252fec8..ecfc417b 100644 --- a/backend/internal/app/usage/types.go +++ b/backend/internal/app/usage/types.go @@ -60,14 +60,11 @@ type LogRecord struct { OutputTokens int CachedInputTokens int CacheCreationTokens int - CacheCreation5mTokens int - CacheCreation1hTokens int ReasoningOutputTokens int InputPrice float64 OutputPrice float64 CachedInputPrice float64 CacheCreationPrice float64 - CacheCreation1hPrice float64 InputCost float64 OutputCost float64 CachedInputCost float64 @@ -80,7 +77,6 @@ type LogRecord struct { SellRate float64 // 快照:本次生效的销售倍率(0 表示未启用 markup) AccountRateMultiplier float64 // 快照:本次生效的 account_rate ServiceTier string - ImageSize string // 图像生成请求的实际出图尺寸("WxH"),非图像请求留空 Stream bool DurationMs int64 FirstTokenMs int64 diff --git a/backend/internal/billing/recorder.go b/backend/internal/billing/recorder.go index 26455caa..b0b0977b 100644 --- a/backend/internal/billing/recorder.go +++ b/backend/internal/billing/recorder.go @@ -29,14 +29,11 @@ type UsageRecord struct { OutputTokens int CachedInputTokens int CacheCreationTokens int - CacheCreation5mTokens int - CacheCreation1hTokens int ReasoningOutputTokens int InputPrice float64 OutputPrice float64 CachedInputPrice float64 CacheCreationPrice float64 - CacheCreation1hPrice float64 InputCost float64 OutputCost float64 CachedInputCost float64 @@ -49,7 +46,6 @@ type UsageRecord struct { SellRate float64 // 快照:本次生效的销售倍率(0 表示未启用 markup) AccountRateMultiplier float64 // 快照:本次生效的 account_rate ServiceTier string - ImageSize string // 图像生成请求的实际出图尺寸("WxH"),非图像请求留空 Stream bool DurationMs int64 FirstTokenMs int64 @@ -234,14 +230,11 @@ func usageLogCreate(tx *ent.Tx, rec UsageRecord) *ent.UsageLogCreate { SetOutputTokens(rec.OutputTokens). SetCachedInputTokens(rec.CachedInputTokens). SetCacheCreationTokens(rec.CacheCreationTokens). - SetCacheCreation5mTokens(rec.CacheCreation5mTokens). - SetCacheCreation1hTokens(rec.CacheCreation1hTokens). SetReasoningOutputTokens(rec.ReasoningOutputTokens). SetInputPrice(rec.InputPrice). SetOutputPrice(rec.OutputPrice). SetCachedInputPrice(rec.CachedInputPrice). SetCacheCreationPrice(rec.CacheCreationPrice). - SetCacheCreation1hPrice(rec.CacheCreation1hPrice). SetInputCost(rec.InputCost). SetOutputCost(rec.OutputCost). SetCachedInputCost(rec.CachedInputCost). @@ -254,7 +247,6 @@ func usageLogCreate(tx *ent.Tx, rec UsageRecord) *ent.UsageLogCreate { SetSellRate(rec.SellRate). SetAccountRateMultiplier(rec.AccountRateMultiplier). SetServiceTier(rec.ServiceTier). - SetImageSize(rec.ImageSize). SetStream(rec.Stream). SetDurationMs(rec.DurationMs). SetFirstTokenMs(rec.FirstTokenMs). diff --git a/backend/internal/bootstrap/startup.go b/backend/internal/bootstrap/startup.go index cc7efdee..4cdb53cf 100644 --- a/backend/internal/bootstrap/startup.go +++ b/backend/internal/bootstrap/startup.go @@ -21,9 +21,34 @@ func RunStartupTasks(db *ent.Client, drv *entsql.Driver, apiKeySecret string) { backfillResellerMarkupColumns(drv) migrateAccountState(drv) migrateUserHistoryRefs(drv) + dropUsageLogLegacyDetailColumns(drv) slog.Info("bootstrap_startup_tasks_done") } +func dropUsageLogLegacyDetailColumns(drv *entsql.Driver) { + if drv == nil { + return + } + ctx := context.Background() + statements := []string{ + `ALTER TABLE usage_logs DROP COLUMN IF EXISTS image_size`, + `ALTER TABLE usage_logs DROP COLUMN IF EXISTS cache_creation_5m_tokens`, + `ALTER TABLE usage_logs DROP COLUMN IF EXISTS cache_creation_1h_tokens`, + `ALTER TABLE usage_logs DROP COLUMN IF EXISTS cache_creation_1h_price`, + `ALTER TABLE usage_logs DROP COLUMN IF EXISTS usage_attributes`, + `ALTER TABLE usage_logs DROP COLUMN IF EXISTS usage_metrics`, + `ALTER TABLE usage_logs DROP COLUMN IF EXISTS usage_cost_details`, + } + for _, sql := range statements { + var r entsql.Result + if err := drv.Exec(ctx, sql, []any{}, &r); err != nil { + slog.Warn("bootstrap_usage_log_legacy_column_drop_failed", "sql", sql, sdk.LogFieldError, err) + return + } + } + slog.Info("bootstrap_usage_log_legacy_columns_dropped") +} + // migrateUserHistoryRefs 允许硬删除用户,同时保留历史使用记录和余额流水。 // 用量/计费聚合依赖 usage_logs 的成本快照字段;这里把历史表的 user 外键改为 SET NULL, // 并回填 user_id/user_email 快照,避免删除用户后历史记录丢失归属信息。 diff --git a/backend/internal/infra/store/usage_store.go b/backend/internal/infra/store/usage_store.go index 8c6268dc..cd7b19a7 100644 --- a/backend/internal/infra/store/usage_store.go +++ b/backend/internal/infra/store/usage_store.go @@ -29,14 +29,11 @@ var usageLogListFields = []string{ entusagelog.FieldOutputTokens, entusagelog.FieldCachedInputTokens, entusagelog.FieldCacheCreationTokens, - entusagelog.FieldCacheCreation5mTokens, - entusagelog.FieldCacheCreation1hTokens, entusagelog.FieldReasoningOutputTokens, entusagelog.FieldInputPrice, entusagelog.FieldOutputPrice, entusagelog.FieldCachedInputPrice, entusagelog.FieldCacheCreationPrice, - entusagelog.FieldCacheCreation1hPrice, entusagelog.FieldInputCost, entusagelog.FieldOutputCost, entusagelog.FieldCachedInputCost, @@ -49,7 +46,6 @@ var usageLogListFields = []string{ entusagelog.FieldSellRate, entusagelog.FieldAccountRateMultiplier, entusagelog.FieldServiceTier, - entusagelog.FieldImageSize, entusagelog.FieldStream, entusagelog.FieldDurationMs, entusagelog.FieldFirstTokenMs, @@ -567,14 +563,11 @@ func mapUsageLog(item *ent.UsageLog) appusage.LogRecord { OutputTokens: item.OutputTokens, CachedInputTokens: item.CachedInputTokens, CacheCreationTokens: item.CacheCreationTokens, - CacheCreation5mTokens: item.CacheCreation5mTokens, - CacheCreation1hTokens: item.CacheCreation1hTokens, ReasoningOutputTokens: item.ReasoningOutputTokens, InputPrice: item.InputPrice, OutputPrice: item.OutputPrice, CachedInputPrice: item.CachedInputPrice, CacheCreationPrice: item.CacheCreationPrice, - CacheCreation1hPrice: item.CacheCreation1hPrice, InputCost: item.InputCost, OutputCost: item.OutputCost, CachedInputCost: item.CachedInputCost, @@ -587,7 +580,6 @@ func mapUsageLog(item *ent.UsageLog) appusage.LogRecord { SellRate: item.SellRate, AccountRateMultiplier: item.AccountRateMultiplier, ServiceTier: item.ServiceTier, - ImageSize: item.ImageSize, Stream: item.Stream, DurationMs: item.DurationMs, FirstTokenMs: item.FirstTokenMs, diff --git a/backend/internal/plugin/host_service.go b/backend/internal/plugin/host_service.go index 38d79a7d..f6197b3e 100644 --- a/backend/internal/plugin/host_service.go +++ b/backend/internal/plugin/host_service.go @@ -1179,14 +1179,11 @@ func (h *HostService) recordHostForwardUsage( OutputTokens: usageValues.OutputTokens, CachedInputTokens: usageValues.CachedInputTokens, CacheCreationTokens: usageValues.CacheCreationTokens, - CacheCreation5mTokens: usageValues.CacheCreation5mTokens, - CacheCreation1hTokens: usageValues.CacheCreation1hTokens, ReasoningOutputTokens: usageValues.ReasoningOutputTokens, InputPrice: usageValues.InputPrice, OutputPrice: usageValues.OutputPrice, CachedInputPrice: usageValues.CachedInputPrice, CacheCreationPrice: usageValues.CacheCreationPrice, - CacheCreation1hPrice: usageValues.CacheCreation1hPrice, InputCost: calc.InputCost, OutputCost: calc.OutputCost, CachedInputCost: calc.CachedInputCost, @@ -1198,7 +1195,6 @@ func (h *HostService) recordHostForwardUsage( RateMultiplier: calc.RateMultiplier, AccountRateMultiplier: calc.AccountRateMultiplier, ServiceTier: usageValues.ServiceTier, - ImageSize: usageValues.ImageSize, Endpoint: req.Path, ReasoningEffort: reasoningEffort, Stream: req.Stream, diff --git a/backend/internal/plugin/image_pricing_test.go b/backend/internal/plugin/image_pricing_test.go index 2be3922a..0717c633 100644 --- a/backend/internal/plugin/image_pricing_test.go +++ b/backend/internal/plugin/image_pricing_test.go @@ -9,8 +9,8 @@ import ( func TestImageOutputBillingOverride_UsesConfiguredTier(t *testing.T) { usage := &sdk.Usage{ - ImageSize: "1672x941", OutputCost: 0.40, + Metadata: map[string]string{"openai.image.size": "1672x941"}, } settings := map[string]map[string]string{ "openai": { @@ -29,8 +29,8 @@ func TestImageOutputBillingOverride_UsesConfiguredTier(t *testing.T) { func TestImageOutputBillingOverride_FallsBackWhenTierUnset(t *testing.T) { usage := &sdk.Usage{ - ImageSize: "3840x2160", OutputCost: 0.40, + Metadata: map[string]string{"openai.image.size": "3840x2160"}, } settings := map[string]map[string]string{ "openai": { @@ -43,6 +43,59 @@ func TestImageOutputBillingOverride_FallsBackWhenTierUnset(t *testing.T) { } } +func TestUsageSnapshotFromSDKReadsPluginMetadata(t *testing.T) { + usage := &sdk.Usage{ + InputTokens: 10, + OutputCost: 0.40, + ReasoningEffort: "high", + Metadata: map[string]string{ + "service_tier": "priority", + "openai.image.size": "1672x941", + "openai.image.input_text_tokens": "3", + "openai.image.input_image_tokens": "7", + "openai.image.count": "2", + "openai.image.unit_price": "0.2", + "openai.image.unit": "USD/image", + }, + } + + snap := usageSnapshotFromSDK(usage) + if snap.ServiceTier != "priority" || snap.ImageSize != "1672x941" { + t.Fatalf("snapshot metadata strings = (%q, %q)", snap.ServiceTier, snap.ImageSize) + } + if snap.TextInputTokens != 3 || snap.ImageInputTokens != 7 || snap.ImageCount != 2 { + t.Fatalf("snapshot metadata ints = (%d, %d, %d)", snap.TextInputTokens, snap.ImageInputTokens, snap.ImageCount) + } + if snap.ImageUnitPrice != 0.2 || snap.ImageUnit != "USD/image" { + t.Fatalf("snapshot image price = (%v, %q)", snap.ImageUnitPrice, snap.ImageUnit) + } + if got := resolveReasoningEffort("", usage); got != "high" { + t.Fatalf("resolveReasoningEffort = %q, want high", got) + } +} + +func TestUsageMetadataFromSDKPreservesPluginMetadata(t *testing.T) { + usage := &sdk.Usage{ + Metadata: map[string]string{ + "custom.plugin.value": "kept", + "openai.image.size": "1672x941", + "openai.image.input_text_tokens": "3", + "claude.cache_creation_1h_tokens": "4", + }, + } + + meta := usageMetadataFromSDK(usage, usageSnapshotFromSDK(usage)) + if meta["custom.plugin.value"] != "kept" { + t.Fatalf("custom plugin metadata = %q, want kept", meta["custom.plugin.value"]) + } + if meta["openai.image.size"] != "1672x941" || meta["openai.image.input_text_tokens"] != "3" { + t.Fatalf("openai image metadata not preserved: %+v", meta) + } + if meta["claude.cache_creation_1h_tokens"] != "4" { + t.Fatalf("claude metadata = %q, want 4", meta["claude.cache_creation_1h_tokens"]) + } +} + func TestImageTierForSize(t *testing.T) { tests := []struct { size string diff --git a/backend/internal/plugin/outcome.go b/backend/internal/plugin/outcome.go index 0c44d97b..ea5e0ec0 100644 --- a/backend/internal/plugin/outcome.go +++ b/backend/internal/plugin/outcome.go @@ -303,14 +303,11 @@ func (f *Forwarder) recordUsage(c *gin.Context, state *forwardState, execution f OutputTokens: usageValues.OutputTokens, CachedInputTokens: usageValues.CachedInputTokens, CacheCreationTokens: usageValues.CacheCreationTokens, - CacheCreation5mTokens: usageValues.CacheCreation5mTokens, - CacheCreation1hTokens: usageValues.CacheCreation1hTokens, ReasoningOutputTokens: usageValues.ReasoningOutputTokens, InputPrice: usageValues.InputPrice, OutputPrice: usageValues.OutputPrice, CachedInputPrice: usageValues.CachedInputPrice, CacheCreationPrice: usageValues.CacheCreationPrice, - CacheCreation1hPrice: usageValues.CacheCreation1hPrice, InputCost: calc.InputCost, OutputCost: calc.OutputCost, CachedInputCost: calc.CachedInputCost, @@ -323,7 +320,6 @@ func (f *Forwarder) recordUsage(c *gin.Context, state *forwardState, execution f SellRate: calc.SellRate, AccountRateMultiplier: calc.AccountRateMultiplier, ServiceTier: usageValues.ServiceTier, - ImageSize: usageValues.ImageSize, Stream: state.stream, DurationMs: execution.duration.Milliseconds(), FirstTokenMs: usageValues.FirstTokenMs, diff --git a/backend/internal/plugin/usage_adapter.go b/backend/internal/plugin/usage_adapter.go index 05a6f124..4b1c6294 100644 --- a/backend/internal/plugin/usage_adapter.go +++ b/backend/internal/plugin/usage_adapter.go @@ -12,20 +12,17 @@ type usageSnapshot struct { OutputTokens int CachedInputTokens int CacheCreationTokens int - CacheCreation5mTokens int - CacheCreation1hTokens int ReasoningOutputTokens int TextInputTokens int ImageInputTokens int ImageCount int - InputPrice float64 - OutputPrice float64 - CachedInputPrice float64 - CacheCreationPrice float64 - CacheCreation1hPrice float64 - ImageUnitPrice float64 - ImageUnit string + InputPrice float64 + OutputPrice float64 + CachedInputPrice float64 + CacheCreationPrice float64 + ImageUnitPrice float64 + ImageUnit string InputCost float64 OutputCost float64 @@ -46,58 +43,85 @@ func usageSnapshotFromSDK(usage *sdk.Usage) usageSnapshot { OutputTokens: usage.OutputTokens, CachedInputTokens: usage.CachedInputTokens, CacheCreationTokens: usage.CacheCreationTokens, - CacheCreation5mTokens: usage.CacheCreation5mTokens, - CacheCreation1hTokens: usage.CacheCreation1hTokens, ReasoningOutputTokens: usage.ReasoningOutputTokens, - TextInputTokens: usage.TextInputTokens, - ImageInputTokens: usage.ImageInputTokens, - ImageCount: usage.ImageCount, InputPrice: usage.InputPrice, OutputPrice: usage.OutputPrice, CachedInputPrice: usage.CachedInputPrice, CacheCreationPrice: usage.CacheCreationPrice, - CacheCreation1hPrice: usage.CacheCreation1hPrice, - ImageUnitPrice: usage.ImageUnitPrice, - ImageUnit: usage.ImageUnit, InputCost: usage.InputCost, OutputCost: usage.OutputCost, CachedInputCost: usage.CachedInputCost, CacheCreationCost: usage.CacheCreationCost, - ServiceTier: usage.ServiceTier, - ImageSize: usage.ImageSize, FirstTokenMs: usage.FirstTokenMs, } - if usage.Metadata != nil { - if snap.ImageSize == "" { - snap.ImageSize = usage.Metadata["image_size"] - } - } + meta := usage.Metadata + snap.TextInputTokens = metadataInt(meta, "openai.image.input_text_tokens") + snap.ImageInputTokens = metadataInt(meta, "openai.image.input_image_tokens") + snap.ImageCount = metadataInt(meta, "openai.image.count") + snap.ImageUnitPrice = metadataFloat(meta, "openai.image.unit_price") + snap.ImageUnit = metadataText(meta, "openai.image.unit") + snap.ServiceTier = metadataText(meta, "service_tier", "tier") + snap.ImageSize = metadataText(meta, "openai.image.size") return snap } func usageMetadataFromSDK(usage *sdk.Usage, snap usageSnapshot) map[string]string { meta := map[string]string{} - if usage != nil { - if snap.ImageSize == "" { - putMetadata(meta, "image_size", usage.Metadata["image_size"]) - putMetadata(meta, "image_size", usage.Metadata["resolution"]) - putMetadata(meta, "image_size", usage.Metadata["size"]) + if usage == nil { + return meta + } + + for key, value := range usage.Metadata { + putMetadata(meta, key, value) + } + putMetadata(meta, "openai.image.size", snap.ImageSize) + putMetadataInt(meta, "openai.image.input_text_tokens", snap.TextInputTokens) + putMetadataInt(meta, "openai.image.input_image_tokens", snap.ImageInputTokens) + putMetadataInt(meta, "openai.image.count", snap.ImageCount) + putMetadataFloat(meta, "openai.image.unit_price", snap.ImageUnitPrice) + putMetadata(meta, "openai.image.unit", snap.ImageUnit) + return meta +} + +func metadataText(meta map[string]string, keys ...string) string { + for _, key := range keys { + value := strings.TrimSpace(meta[key]) + if value != "" { + return value } - if snap.ImageUnit == "" { - putMetadata(meta, "image_unit", usage.Metadata["image_unit"]) - putMetadata(meta, "image_unit", usage.Metadata["unit"]) + } + return "" +} + +func metadataInt(meta map[string]string, keys ...string) int { + for _, key := range keys { + raw := strings.TrimSpace(meta[key]) + if raw == "" { + continue + } + if value, err := strconv.Atoi(raw); err == nil { + return value + } + if value, err := strconv.ParseFloat(raw, 64); err == nil { + return int(value) } } + return 0 +} - putMetadata(meta, "image_size", snap.ImageSize) - putMetadataInt(meta, "input_text_tokens", snap.TextInputTokens) - putMetadataInt(meta, "input_image_tokens", snap.ImageInputTokens) - putMetadataInt(meta, "images", snap.ImageCount) - putMetadataFloat(meta, "image_unit_price", snap.ImageUnitPrice) - putMetadata(meta, "image_unit", snap.ImageUnit) - return meta +func metadataFloat(meta map[string]string, keys ...string) float64 { + for _, key := range keys { + raw := strings.TrimSpace(meta[key]) + if raw == "" { + continue + } + if value, err := strconv.ParseFloat(raw, 64); err == nil { + return value + } + } + return 0 } func putMetadata(meta map[string]string, key, value string) { diff --git a/backend/internal/server/dto/usage.go b/backend/internal/server/dto/usage.go index 7c0258bf..5e13acf0 100644 --- a/backend/internal/server/dto/usage.go +++ b/backend/internal/server/dto/usage.go @@ -20,14 +20,11 @@ type UsageLogResp struct { OutputTokens int `json:"output_tokens"` CachedInputTokens int `json:"cached_input_tokens"` CacheCreationTokens int `json:"cache_creation_tokens"` - CacheCreation5mTokens int `json:"cache_creation_5m_tokens"` - CacheCreation1hTokens int `json:"cache_creation_1h_tokens"` ReasoningOutputTokens int `json:"reasoning_output_tokens"` InputPrice float64 `json:"input_price"` OutputPrice float64 `json:"output_price"` CachedInputPrice float64 `json:"cached_input_price"` CacheCreationPrice float64 `json:"cache_creation_price"` - CacheCreation1hPrice float64 `json:"cache_creation_1h_price"` InputCost float64 `json:"input_cost"` OutputCost float64 `json:"output_cost"` CachedInputCost float64 `json:"cached_input_cost"` @@ -40,7 +37,6 @@ type UsageLogResp struct { SellRate float64 `json:"sell_rate"` // 销售倍率快照 AccountRateMultiplier float64 `json:"account_rate_multiplier"` // 账号倍率快照 ServiceTier string `json:"service_tier,omitempty"` - ImageSize string `json:"image_size,omitempty"` // 图像生成实际出图尺寸("WxH"),非图像请求空 Stream bool `json:"stream"` DurationMs int64 `json:"duration_ms"` FirstTokenMs int64 `json:"first_token_ms"` @@ -65,12 +61,9 @@ type CustomerUsageLogResp struct { OutputTokens int `json:"output_tokens"` CachedInputTokens int `json:"cached_input_tokens"` CacheCreationTokens int `json:"cache_creation_tokens"` - CacheCreation5mTokens int `json:"cache_creation_5m_tokens"` - CacheCreation1hTokens int `json:"cache_creation_1h_tokens"` ReasoningOutputTokens int `json:"reasoning_output_tokens"` BilledCost float64 `json:"cost"` // 客户视角:"本次消耗 = X 美元" ServiceTier string `json:"service_tier,omitempty"` - ImageSize string `json:"image_size,omitempty"` // 图像生成实际出图尺寸("WxH"),非图像请求空 Stream bool `json:"stream"` DurationMs int64 `json:"duration_ms"` FirstTokenMs int64 `json:"first_token_ms"` diff --git a/backend/internal/server/handler/usage_handler_mapper.go b/backend/internal/server/handler/usage_handler_mapper.go index 48524afc..2037e8db 100644 --- a/backend/internal/server/handler/usage_handler_mapper.go +++ b/backend/internal/server/handler/usage_handler_mapper.go @@ -26,14 +26,11 @@ func toUsageLogResp(record appusage.LogRecord) dto.UsageLogResp { OutputTokens: record.OutputTokens, CachedInputTokens: record.CachedInputTokens, CacheCreationTokens: record.CacheCreationTokens, - CacheCreation5mTokens: record.CacheCreation5mTokens, - CacheCreation1hTokens: record.CacheCreation1hTokens, ReasoningOutputTokens: record.ReasoningOutputTokens, InputPrice: record.InputPrice, OutputPrice: record.OutputPrice, CachedInputPrice: record.CachedInputPrice, CacheCreationPrice: record.CacheCreationPrice, - CacheCreation1hPrice: record.CacheCreation1hPrice, InputCost: record.InputCost, OutputCost: record.OutputCost, CachedInputCost: record.CachedInputCost, @@ -46,7 +43,6 @@ func toUsageLogResp(record appusage.LogRecord) dto.UsageLogResp { SellRate: record.SellRate, AccountRateMultiplier: record.AccountRateMultiplier, ServiceTier: record.ServiceTier, - ImageSize: record.ImageSize, Stream: record.Stream, DurationMs: record.DurationMs, FirstTokenMs: record.FirstTokenMs, @@ -72,12 +68,9 @@ func toCustomerUsageLogResp(record appusage.LogRecord) dto.CustomerUsageLogResp OutputTokens: record.OutputTokens, CachedInputTokens: record.CachedInputTokens, CacheCreationTokens: record.CacheCreationTokens, - CacheCreation5mTokens: record.CacheCreation5mTokens, - CacheCreation1hTokens: record.CacheCreation1hTokens, ReasoningOutputTokens: record.ReasoningOutputTokens, BilledCost: record.BilledCost, ServiceTier: record.ServiceTier, - ImageSize: record.ImageSize, Stream: record.Stream, DurationMs: record.DurationMs, FirstTokenMs: record.FirstTokenMs, diff --git a/web/src/shared/columns/usageColumns.tsx b/web/src/shared/columns/usageColumns.tsx index 0cee0e3f..0a438bb4 100644 --- a/web/src/shared/columns/usageColumns.tsx +++ b/web/src/shared/columns/usageColumns.tsx @@ -332,7 +332,7 @@ function rowMetrics(row: UsageRow): MetricDisplay[] { { key: 'cached_input_tokens', label: '缓存读取 Token', kind: 'token', unit: 'token', value: row.cached_input_tokens }, { key: 'cache_creation_tokens', label: '缓存写入 Token', kind: 'token', unit: 'token', value: cacheCreation }, ]; - const imageCount = usageMetadataNumber(row.usage_metadata ?? {}, ['images', 'image_count']); + const imageCount = usageMetadataNumber(row.usage_metadata ?? {}, ['openai.image.count']); if (imageCount > 0) { metrics.push({ key: 'images', label: '图片数量', kind: 'image', unit: 'image', value: imageCount }); } @@ -341,33 +341,18 @@ function rowMetrics(row: UsageRow): MetricDisplay[] { function buildUsageRecordContext(row: UsageRow, customerScope: boolean) { const usageMetadata = row.usage_metadata ?? {}; - const imageSize = firstText( - row.image_size, - usageMetadataValue(usageMetadata, ['image_size', 'resolution', 'size']), - ); - const serviceTier = firstText( - row.service_tier, - usageMetadataValue(usageMetadata, ['service_tier', 'tier']), - ); - const reasoningEffort = firstText( - (row as Partial).reasoning_effort, - usageMetadataValue(usageMetadata, ['reasoning_effort', 'reasoning']), - ); + const serviceTier = firstText(row.service_tier); + const reasoningEffort = firstText((row as Partial).reasoning_effort); const reasoningTokens = (row as Partial).reasoning_output_tokens; - const inputTextTokens = usageMetadataNumber(usageMetadata, ['input_text_tokens', 'text_input_tokens', 'text_tokens']); - const inputImageTokens = usageMetadataNumber(usageMetadata, ['input_image_tokens', 'image_input_tokens', 'image_tokens']); - const images = usageMetadataNumber(usageMetadata, ['images', 'image_count']); const ctx: Record = { record: row, customerScope, - usageMetadata, usage_metadata: usageMetadata, // 常用的行级别字段做扁平化,方便插件扩展渲染器直接取值。 model: row.model, platform: row.platform, service_tier: serviceTier, - image_size: imageSize, endpoint: row.endpoint, stream: row.stream, created_at: row.created_at, @@ -377,9 +362,6 @@ function buildUsageRecordContext(row: UsageRow, customerScope: boolean) { if (typeof reasoningTokens === 'number' && reasoningTokens > 0) { ctx.reasoning_output_tokens = reasoningTokens; } - if (inputTextTokens > 0) ctx.input_text_tokens = inputTextTokens; - if (inputImageTokens > 0) ctx.input_image_tokens = inputImageTokens; - if (images > 0) ctx.images = images; return ctx; } @@ -455,6 +437,9 @@ function buildResellerCostColumn(t: TFunction, adminView: boolean): UsageColumnC {row.output_price > 0 && ( )} + {row.cache_creation_price > 0 && ( + + )} {row.cached_input_cost > 0 && ( )} @@ -579,7 +564,7 @@ export function useUsageColumns(opts?: { customerScope?: boolean; adminView?: bo const metaContext = buildUsageRecordContext(row, customerScope); const fallbackMeta = (() => { if (PluginUsageModelMeta) return null; - const imageSize = typeof metaContext.image_size === 'string' ? metaContext.image_size : ''; + const imageSize = usageMetadataValue(row.usage_metadata ?? {}, ['openai.image.size']) ?? ''; if (imageSize) { return ( Date: Thu, 28 May 2026 02:20:23 +0800 Subject: [PATCH 08/19] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E6=B8=B8=E6=A0=87=E7=9A=84=E5=88=86=E9=A1=B5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=9B=B4=E6=96=B0=E4=BD=BF=E7=94=A8=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=9F=A5=E8=AF=A2=E9=80=BB=E8=BE=91=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/ent/migrate/schema.go | 12 ++++ backend/ent/schema/usagelog.go | 10 +++ backend/internal/app/usage/service.go | 46 +++++++++++--- backend/internal/app/usage/types.go | 16 +++-- .../infra/store/apikey_usage_helper.go | 14 ++++- backend/internal/infra/store/usage_store.go | 62 +++++++++++-------- .../internal/infra/store/usage_store_test.go | 36 +++++++---- backend/internal/server/dto/usage.go | 1 + .../server/handler/usage_handler_routes.go | 15 ++++- backend/internal/server/response/response.go | 11 ++++ web/src/pages/admin/UsagePage.tsx | 20 +++--- web/src/pages/user/UserUsageContent.tsx | 20 +++--- .../components/TablePaginationFooter.tsx | 15 +++-- .../shared/components/UsageRecordsTable.tsx | 6 ++ web/src/shared/hooks/useCursorPagination.ts | 46 ++++++++++++++ web/src/shared/types/index.ts | 4 ++ 16 files changed, 255 insertions(+), 79 deletions(-) create mode 100644 web/src/shared/hooks/useCursorPagination.ts diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index d5c81951..70987119 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -339,6 +339,18 @@ var ( OnDelete: schema.SetNull, }, }, + Indexes: []*schema.Index{ + { + Name: "usage_log_model", + Unique: false, + Columns: []*schema.Column{UsageLogsColumns[2]}, + }, + { + Name: "usage_log_api_key", + Unique: false, + Columns: []*schema.Column{UsageLogsColumns[35]}, + }, + }, } // UsersColumns holds the columns for the "users" table. UsersColumns = []*schema.Column{ diff --git a/backend/ent/schema/usagelog.go b/backend/ent/schema/usagelog.go index 8919a3c8..6974cdae 100644 --- a/backend/ent/schema/usagelog.go +++ b/backend/ent/schema/usagelog.go @@ -5,6 +5,7 @@ import ( "entgo.io/ent/dialect" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" + "entgo.io/ent/schema/index" ) // UsageLog 使用日志(只追加) @@ -81,3 +82,12 @@ func (UsageLog) Edges() []ent.Edge { edge.From("group", Group.Type).Ref("usage_logs").Unique(), } } + +func (UsageLog) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("model"). + StorageKey("usage_log_model"), + index.Edges("api_key"). + StorageKey("usage_log_api_key"), + } +} diff --git a/backend/internal/app/usage/service.go b/backend/internal/app/usage/service.go index 77364826..07c63381 100644 --- a/backend/internal/app/usage/service.go +++ b/backend/internal/app/usage/service.go @@ -20,10 +20,13 @@ func NewService(repo Repository) *Service { // ListUser 查询当前用户的使用记录。 func (s *Service) ListUser(ctx context.Context, userID int64, filter ListFilter) (ListResult, error) { page, pageSize := NormalizePage(filter.Page, filter.PageSize) + if filter.BeforeID <= 0 { + page = 1 + } filter.Page = page filter.PageSize = pageSize - list, total, err := s.repo.ListUser(ctx, userID, filter) + list, hasMore, nextCursor, err := s.repo.ListUser(ctx, userID, filter) if err != nil { sdk.LoggerFromContext(ctx).Error("usage_query_failed", "scope", "user_list", @@ -33,10 +36,13 @@ func (s *Service) ListUser(ctx context.Context, userID int64, filter ListFilter) } return ListResult{ - List: list, - Total: total, - Page: page, - PageSize: pageSize, + List: list, + Total: usageListTotal(page, pageSize, len(list), hasMore), + Page: page, + PageSize: pageSize, + HasMore: hasMore, + NextCursor: nextCursor, + TotalExact: !hasMore, }, nil } @@ -55,10 +61,13 @@ func (s *Service) UserStats(ctx context.Context, userID int64, filter StatsFilte // ListAdmin 查询管理员使用记录列表。 func (s *Service) ListAdmin(ctx context.Context, filter ListFilter) (ListResult, error) { page, pageSize := NormalizePage(filter.Page, filter.PageSize) + if filter.BeforeID <= 0 { + page = 1 + } filter.Page = page filter.PageSize = pageSize - list, total, err := s.repo.ListAdmin(ctx, filter) + list, hasMore, nextCursor, err := s.repo.ListAdmin(ctx, filter) if err != nil { sdk.LoggerFromContext(ctx).Error("usage_query_failed", "scope", "admin_list", @@ -67,13 +76,30 @@ func (s *Service) ListAdmin(ctx context.Context, filter ListFilter) (ListResult, } return ListResult{ - List: list, - Total: total, - Page: page, - PageSize: pageSize, + List: list, + Total: usageListTotal(page, pageSize, len(list), hasMore), + Page: page, + PageSize: pageSize, + HasMore: hasMore, + NextCursor: nextCursor, + TotalExact: !hasMore, }, nil } +func usageListTotal(page, pageSize, listLen int, hasMore bool) int64 { + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = defaultPageSize + } + total := int64((page-1)*pageSize + listLen) + if hasMore { + total++ + } + return total +} + // StatsByModel 按模型分组统计。 func (s *Service) StatsByModel(ctx context.Context, filter StatsFilter) ([]ModelStats, error) { stats, err := s.repo.StatsByModel(ctx, filter) diff --git a/backend/internal/app/usage/types.go b/backend/internal/app/usage/types.go index ecfc417b..ed1c321e 100644 --- a/backend/internal/app/usage/types.go +++ b/backend/internal/app/usage/types.go @@ -6,6 +6,7 @@ import "context" type ListFilter struct { Page int PageSize int + BeforeID int64 UserID *int64 APIKeyID *int64 AccountID *int64 @@ -90,10 +91,13 @@ type LogRecord struct { // ListResult 使用记录列表结果。 type ListResult struct { - List []LogRecord - Total int64 - Page int - PageSize int + List []LogRecord + Total int64 + Page int + PageSize int + HasMore bool + NextCursor *int64 + TotalExact bool } // Summary 汇总统计。 @@ -185,8 +189,8 @@ type TrendBucket struct { // Repository 使用记录仓储接口。 type Repository interface { - ListUser(context.Context, int64, ListFilter) ([]LogRecord, int64, error) - ListAdmin(context.Context, ListFilter) ([]LogRecord, int64, error) + ListUser(context.Context, int64, ListFilter) ([]LogRecord, bool, *int64, error) + ListAdmin(context.Context, ListFilter) ([]LogRecord, bool, *int64, error) SummaryUser(context.Context, int64, StatsFilter) (Summary, error) SummaryAdmin(context.Context, StatsFilter) (Summary, error) StatsByModel(context.Context, StatsFilter) ([]ModelStats, error) diff --git a/backend/internal/infra/store/apikey_usage_helper.go b/backend/internal/infra/store/apikey_usage_helper.go index bea461a2..7a464a8b 100644 --- a/backend/internal/infra/store/apikey_usage_helper.go +++ b/backend/internal/infra/store/apikey_usage_helper.go @@ -4,8 +4,10 @@ import ( "context" "time" + "entgo.io/ent/dialect/sql" + "github.com/DouDOU-start/airgate-core/ent" - entapikey "github.com/DouDOU-start/airgate-core/ent/apikey" + "github.com/DouDOU-start/airgate-core/ent/predicate" entusagelog "github.com/DouDOU-start/airgate-core/ent/usagelog" ) @@ -28,7 +30,7 @@ func queryAPIKeyUsage(ctx context.Context, db *ent.Client, keyIDs []int, todaySt var todayRows []costRow if err := db.UsageLog.Query(). Where( - entusagelog.HasAPIKeyWith(entapikey.IDIn(keyIDs...)), + usageLogColumnIn(entusagelog.APIKeyColumn, keyIDs), entusagelog.CreatedAtGTE(todayStart), ). GroupBy(entusagelog.ForeignKeys[0]). @@ -43,7 +45,7 @@ func queryAPIKeyUsage(ctx context.Context, db *ent.Client, keyIDs []int, todaySt var thirtyDayRows []costRow if err := db.UsageLog.Query(). Where( - entusagelog.HasAPIKeyWith(entapikey.IDIn(keyIDs...)), + usageLogColumnIn(entusagelog.APIKeyColumn, keyIDs), entusagelog.CreatedAtGTE(thirtyDaysAgo), ). GroupBy(entusagelog.ForeignKeys[0]). @@ -57,3 +59,9 @@ func queryAPIKeyUsage(ctx context.Context, db *ent.Client, keyIDs []int, todaySt return todayMap, thirtyDayMap, nil } + +func usageLogColumnIn(column string, values []int) predicate.UsageLog { + return predicate.UsageLog(func(s *sql.Selector) { + s.Where(sql.InInts(s.C(column), values...)) + }) +} diff --git a/backend/internal/infra/store/usage_store.go b/backend/internal/infra/store/usage_store.go index cd7b19a7..3d841f83 100644 --- a/backend/internal/infra/store/usage_store.go +++ b/backend/internal/infra/store/usage_store.go @@ -5,9 +5,10 @@ import ( "sort" "time" + "entgo.io/ent/dialect/sql" + "github.com/DouDOU-start/airgate-core/ent" entaccount "github.com/DouDOU-start/airgate-core/ent/account" - entapikey "github.com/DouDOU-start/airgate-core/ent/apikey" entgroup "github.com/DouDOU-start/airgate-core/ent/group" "github.com/DouDOU-start/airgate-core/ent/predicate" entusagelog "github.com/DouDOU-start/airgate-core/ent/usagelog" @@ -69,14 +70,12 @@ func NewUsageStore(db *ent.Client) *UsageStore { } // ListUser 查询用户使用记录。 -func (s *UsageStore) ListUser(ctx context.Context, userID int64, filter appusage.ListFilter) ([]appusage.LogRecord, int64, error) { +func (s *UsageStore) ListUser(ctx context.Context, userID int64, filter appusage.ListFilter) ([]appusage.LogRecord, bool, *int64, error) { query := s.db.UsageLog.Query(). Where(usageUserPredicate(userID)) query = applyUsageListFilter(query, filter) - - total, err := query.Count(ctx) - if err != nil { - return nil, 0, err + if filter.BeforeID > 0 { + query = query.Where(entusagelog.IDLT(int(filter.BeforeID))) } logs, err := query. @@ -85,32 +84,25 @@ func (s *UsageStore) ListUser(ctx context.Context, userID int64, filter appusage WithAPIKey(). WithAccount(). WithGroup(). - Offset((filter.Page - 1) * filter.PageSize). - Limit(filter.PageSize). + Limit(filter.PageSize + 1). Order(ent.Desc(entusagelog.FieldID)). All(ctx) if err != nil { - return nil, 0, err + return nil, false, nil, err } - result := make([]appusage.LogRecord, 0, len(logs)) - for _, item := range logs { - result = append(result, mapUsageLog(item)) - } - return result, int64(total), nil + return mapUsageLogPage(logs, filter.PageSize) } // ListAdmin 查询管理员使用记录。 -func (s *UsageStore) ListAdmin(ctx context.Context, filter appusage.ListFilter) ([]appusage.LogRecord, int64, error) { +func (s *UsageStore) ListAdmin(ctx context.Context, filter appusage.ListFilter) ([]appusage.LogRecord, bool, *int64, error) { query := s.db.UsageLog.Query() if filter.UserID != nil { query = query.Where(usageUserPredicate(*filter.UserID)) } query = applyUsageListFilter(query, filter) - - total, err := query.Count(ctx) - if err != nil { - return nil, 0, err + if filter.BeforeID > 0 { + query = query.Where(entusagelog.IDLT(int(filter.BeforeID))) } logs, err := query. @@ -119,19 +111,33 @@ func (s *UsageStore) ListAdmin(ctx context.Context, filter appusage.ListFilter) WithAPIKey(). WithAccount(). WithGroup(). - Offset((filter.Page - 1) * filter.PageSize). - Limit(filter.PageSize). + Limit(filter.PageSize + 1). Order(ent.Desc(entusagelog.FieldID)). All(ctx) if err != nil { - return nil, 0, err + return nil, false, nil, err + } + + return mapUsageLogPage(logs, filter.PageSize) +} + +func mapUsageLogPage(logs []*ent.UsageLog, pageSize int) ([]appusage.LogRecord, bool, *int64, error) { + hasMore := len(logs) > pageSize + if hasMore { + logs = logs[:pageSize] } result := make([]appusage.LogRecord, 0, len(logs)) for _, item := range logs { result = append(result, mapUsageLog(item)) } - return result, int64(total), nil + + var nextCursor *int64 + if hasMore && len(result) > 0 { + cursor := result[len(result)-1].ID + nextCursor = &cursor + } + return result, hasMore, nextCursor, nil } // SummaryUser 查询用户汇总统计。 @@ -463,6 +469,12 @@ func usageUserPredicate(userID int64) predicate.UsageLog { ) } +func usageLogColumnEQ(column string, value int) predicate.UsageLog { + return predicate.UsageLog(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(column), value)) + }) +} + func coalesceString(primary, fallback string) string { if primary != "" { return primary @@ -472,7 +484,7 @@ func coalesceString(primary, fallback string) string { func applyUsageListFilter(query *ent.UsageLogQuery, filter appusage.ListFilter) *ent.UsageLogQuery { if filter.APIKeyID != nil { - query = query.Where(entusagelog.HasAPIKeyWith(entapikey.IDEQ(int(*filter.APIKeyID)))) + query = query.Where(usageLogColumnEQ(entusagelog.APIKeyColumn, int(*filter.APIKeyID))) } if filter.AccountID != nil { query = query.Where(entusagelog.HasAccountWith(entaccount.IDEQ(int(*filter.AccountID)))) @@ -492,7 +504,7 @@ func applyUsageListFilter(query *ent.UsageLogQuery, filter appusage.ListFilter) func applyUsageStatsFilter(query *ent.UsageLogQuery, filter appusage.StatsFilter) *ent.UsageLogQuery { if filter.APIKeyID != nil { - query = query.Where(entusagelog.HasAPIKeyWith(entapikey.IDEQ(int(*filter.APIKeyID)))) + query = query.Where(usageLogColumnEQ(entusagelog.APIKeyColumn, int(*filter.APIKeyID))) } if filter.Platform != "" { query = query.Where(entusagelog.PlatformEQ(filter.Platform)) diff --git a/backend/internal/infra/store/usage_store_test.go b/backend/internal/infra/store/usage_store_test.go index 071b12d4..c6f8d902 100644 --- a/backend/internal/infra/store/usage_store_test.go +++ b/backend/internal/infra/store/usage_store_test.go @@ -36,41 +36,53 @@ func TestUsageStoreListPaginationUsesStableIDOrder(t *testing.T) { store := NewUsageStore(db) t.Run("admin list", func(t *testing.T) { - page1, total, err := store.ListAdmin(ctx, appusage.ListFilter{Page: 1, PageSize: 2}) + page1, hasMore, nextCursor, err := store.ListAdmin(ctx, appusage.ListFilter{Page: 1, PageSize: 2}) if err != nil { t.Fatalf("ListAdmin page 1 returned error: %v", err) } - if total != 3 { - t.Fatalf("ListAdmin page 1 total = %d, want 3", total) + if !hasMore { + t.Fatalf("ListAdmin page 1 hasMore = false, want true") + } + if nextCursor == nil || *nextCursor != 2 { + t.Fatalf("ListAdmin page 1 nextCursor = %v, want 2", nextCursor) } assertLogIDs(t, page1, 3, 2) - page2, total, err := store.ListAdmin(ctx, appusage.ListFilter{Page: 2, PageSize: 2}) + page2, hasMore, nextCursor, err := store.ListAdmin(ctx, appusage.ListFilter{Page: 2, PageSize: 2, BeforeID: *nextCursor}) if err != nil { t.Fatalf("ListAdmin page 2 returned error: %v", err) } - if total != 3 { - t.Fatalf("ListAdmin page 2 total = %d, want 3", total) + if hasMore { + t.Fatalf("ListAdmin page 2 hasMore = true, want false") + } + if nextCursor != nil { + t.Fatalf("ListAdmin page 2 nextCursor = %v, want nil", *nextCursor) } assertLogIDs(t, page2, 1) }) t.Run("user list", func(t *testing.T) { - page1, total, err := store.ListUser(ctx, int64(user.ID), appusage.ListFilter{Page: 1, PageSize: 2}) + page1, hasMore, nextCursor, err := store.ListUser(ctx, int64(user.ID), appusage.ListFilter{Page: 1, PageSize: 2}) if err != nil { t.Fatalf("ListUser page 1 returned error: %v", err) } - if total != 3 { - t.Fatalf("ListUser page 1 total = %d, want 3", total) + if !hasMore { + t.Fatalf("ListUser page 1 hasMore = false, want true") + } + if nextCursor == nil || *nextCursor != 2 { + t.Fatalf("ListUser page 1 nextCursor = %v, want 2", nextCursor) } assertLogIDs(t, page1, 3, 2) - page2, total, err := store.ListUser(ctx, int64(user.ID), appusage.ListFilter{Page: 2, PageSize: 2}) + page2, hasMore, nextCursor, err := store.ListUser(ctx, int64(user.ID), appusage.ListFilter{Page: 2, PageSize: 2, BeforeID: *nextCursor}) if err != nil { t.Fatalf("ListUser page 2 returned error: %v", err) } - if total != 3 { - t.Fatalf("ListUser page 2 total = %d, want 3", total) + if hasMore { + t.Fatalf("ListUser page 2 hasMore = true, want false") + } + if nextCursor != nil { + t.Fatalf("ListUser page 2 nextCursor = %v, want nil", *nextCursor) } assertLogIDs(t, page2, 1) }) diff --git a/backend/internal/server/dto/usage.go b/backend/internal/server/dto/usage.go index 5e13acf0..53792e69 100644 --- a/backend/internal/server/dto/usage.go +++ b/backend/internal/server/dto/usage.go @@ -76,6 +76,7 @@ type CustomerUsageLogResp struct { // UsageQuery 使用记录查询参数 type UsageQuery struct { PageReq + BeforeID *int64 `form:"before_id"` UserID *int64 `form:"user_id"` APIKeyID *int64 `form:"api_key_id"` AccountID *int64 `form:"account_id"` diff --git a/backend/internal/server/handler/usage_handler_routes.go b/backend/internal/server/handler/usage_handler_routes.go index c6f0a87f..6f9b88f5 100644 --- a/backend/internal/server/handler/usage_handler_routes.go +++ b/backend/internal/server/handler/usage_handler_routes.go @@ -34,6 +34,7 @@ func (h *UsageHandler) UserUsage(c *gin.Context) { result, err := h.service.ListUser(c.Request.Context(), int64(userID), appusage.ListFilter{ Page: query.Page, PageSize: query.PageSize, + BeforeID: ptrInt64Value(query.BeforeID), APIKeyID: apiKeyFilter, AccountID: query.AccountID, GroupID: query.GroupID, @@ -56,7 +57,7 @@ func (h *UsageHandler) UserUsage(c *gin.Context) { for _, item := range result.List { list = append(list, toCustomerUsageLogResp(item)) } - response.Success(c, response.PagedData(list, result.Total, result.Page, result.PageSize)) + response.Success(c, response.CursorPagedData(list, result.Total, result.Page, result.PageSize, result.HasMore, result.NextCursor, result.TotalExact)) return } @@ -69,7 +70,7 @@ func (h *UsageHandler) UserUsage(c *gin.Context) { resp.AccountRateMultiplier = 0 list = append(list, resp) } - response.Success(c, response.PagedData(list, result.Total, result.Page, result.PageSize)) + response.Success(c, response.CursorPagedData(list, result.Total, result.Page, result.PageSize, result.HasMore, result.NextCursor, result.TotalExact)) } // UserUsageStats 用户聚合统计。 @@ -238,6 +239,7 @@ func (h *UsageHandler) AdminUsage(c *gin.Context) { result, err := h.service.ListAdmin(c.Request.Context(), appusage.ListFilter{ Page: query.Page, PageSize: query.PageSize, + BeforeID: ptrInt64Value(query.BeforeID), UserID: query.UserID, APIKeyID: query.APIKeyID, AccountID: query.AccountID, @@ -258,7 +260,14 @@ func (h *UsageHandler) AdminUsage(c *gin.Context) { for _, item := range result.List { list = append(list, toUsageLogResp(item)) } - response.Success(c, response.PagedData(list, result.Total, result.Page, result.PageSize)) + response.Success(c, response.CursorPagedData(list, result.Total, result.Page, result.PageSize, result.HasMore, result.NextCursor, result.TotalExact)) +} + +func ptrInt64Value(value *int64) int64 { + if value == nil { + return 0 + } + return *value } // AdminUsageStats 管理员聚合统计。 diff --git a/backend/internal/server/response/response.go b/backend/internal/server/response/response.go index c653553f..0919f406 100644 --- a/backend/internal/server/response/response.go +++ b/backend/internal/server/response/response.go @@ -72,3 +72,14 @@ func PagedData(list interface{}, total int64, page, pageSize int) map[string]int "page_size": pageSize, } } + +// CursorPagedData 构建基于游标的分页响应数据。 +func CursorPagedData(list interface{}, total int64, page, pageSize int, hasMore bool, nextCursor *int64, totalExact bool) map[string]interface{} { + data := PagedData(list, total, page, pageSize) + data["has_more"] = hasMore + data["total_exact"] = totalExact + if nextCursor != nil { + data["next_cursor"] = *nextCursor + } + return data +} diff --git a/web/src/pages/admin/UsagePage.tsx b/web/src/pages/admin/UsagePage.tsx index a7223083..040c26ac 100644 --- a/web/src/pages/admin/UsagePage.tsx +++ b/web/src/pages/admin/UsagePage.tsx @@ -5,7 +5,7 @@ import { Card, ComboBox, Input, ListBox, Select, Tabs } from '@heroui/react'; import { usageApi } from '../../shared/api/usage'; import { usersApi } from '../../shared/api/users'; import { apikeysApi } from '../../shared/api/apikeys'; -import { usePagination } from '../../shared/hooks/usePagination'; +import { useCursorPagination } from '../../shared/hooks/useCursorPagination'; import { usePlatforms } from '../../shared/hooks/usePlatforms'; import { useDebouncedValue } from '../../shared/hooks/useDebouncedValue'; import { useDeferredActivation } from '../../shared/hooks/useDeferredActivation'; @@ -388,7 +388,7 @@ function TokenTrendCard({ export default function UsagePage() { const { t } = useTranslation(); - const { page, setPage, pageSize, setPageSize } = usePagination(20, 'admin.usage'); + const { beforeId, page, setPage, pageSize, setPageSize, resetCursorPagination } = useCursorPagination(20, 'admin.usage'); const [filters, setFilters] = useState>({}); const [statsGroupBy, setStatsGroupBy] = useState('model'); const [granularity, setGranularity] = useState('hour'); @@ -401,9 +401,9 @@ export default function UsagePage() { const handleModelChange = useCallback((model: string) => { const nextModel = model || undefined; - setPage(1); + resetCursorPagination(); setFilters((prev) => (prev.model === nextModel ? prev : { ...prev, model: nextModel })); - }, [setPage]); + }, [resetCursorPagination]); // 用户搜索 const [userKeyword, setUserKeyword] = useState(''); @@ -475,8 +475,9 @@ export default function UsagePage() { const queryParams = useMemo(() => ({ page, page_size: pageSize, + before_id: beforeId, ...filters, - }), [filters, page, pageSize]); + }), [beforeId, filters, page, pageSize]); // 使用记录列表 const { @@ -555,7 +556,7 @@ export default function UsagePage() { ? (value ? Number(value) : undefined) : value || undefined; setFilters((prev) => ({ ...prev, [key]: nextValue })); - setPage(1); + resetCursorPagination(); } const activeStats = pageActive ? stats : undefined; @@ -686,6 +687,7 @@ export default function UsagePage() { ] as UsageColumnConfig[]; }, [sharedColumns, t]); const total = data?.total ?? 0; + const canUseCursor = pageActive && !isPlaceholderData; return (
@@ -758,7 +760,7 @@ export default function UsagePage() { label={t('usage.time_range')} startDate={filters.start_date} onChange={(startDate, endDate) => { - setPage(1); + resetCursorPagination(); setFilters((prev) => ({ ...prev, start_date: startDate, end_date: endDate })); }} /> @@ -936,14 +938,16 @@ export default function UsagePage() { emptyTitle={t('common.no_data')} highlightNewRows={pageActive && autoRefreshEnabled && page === 1} highlightResetKey={JSON.stringify({ ...filters, page, pageSize })} + hasMore={canUseCursor ? data?.has_more : false} isLoading={!pageActive || isLoading} page={page} pageSize={pageSize} rows={pageActive ? data?.list ?? [] : []} - setPage={setPage} + setPage={(nextPage) => setPage(nextPage, canUseCursor ? data?.next_cursor : undefined)} setPageSize={setPageSize} suppressHighlight={!pageActive || isPlaceholderData} total={pageActive ? total : 0} + totalExact={canUseCursor ? data?.total_exact : true} />
); diff --git a/web/src/pages/user/UserUsageContent.tsx b/web/src/pages/user/UserUsageContent.tsx index 23b0ba7a..9ed82c27 100644 --- a/web/src/pages/user/UserUsageContent.tsx +++ b/web/src/pages/user/UserUsageContent.tsx @@ -5,7 +5,7 @@ import { Button, Card, ListBox, Meter, Select } from '@heroui/react'; import { usageApi } from '../../shared/api/usage'; import { apikeysApi } from '../../shared/api/apikeys'; import { queryKeys } from '../../shared/queryKeys'; -import { usePagination } from '../../shared/hooks/usePagination'; +import { useCursorPagination } from '../../shared/hooks/useCursorPagination'; import { usePlatforms } from '../../shared/hooks/usePlatforms'; import { useAuth } from '../../app/providers/AuthProvider'; import { useToast } from '../../shared/ui'; @@ -185,7 +185,7 @@ export default function UserUsageContent() { const { t } = useTranslation(); const { user } = useAuth(); const customerScope = !!user?.api_key_id; - const { page, setPage, pageSize, setPageSize } = usePagination(20, 'user.usage'); + const { beforeId, page, setPage, pageSize, setPageSize, resetCursorPagination } = useCursorPagination(20, 'user.usage'); const [filters, setFilters] = useState>({}); const [autoRefresh, setAutoRefresh] = usePersistentAutoRefresh(USER_USAGE_AUTO_UPDATE_STORAGE_KEY, 0, USER_AUTO_REFRESH_OPTIONS); const autoRefreshEnabled = autoRefresh > 0; @@ -194,15 +194,16 @@ export default function UserUsageContent() { const handleModelChange = useCallback((model: string) => { const nextModel = model || undefined; - setPage(1); + resetCursorPagination(); setFilters((prev) => (prev.model === nextModel ? prev : { ...prev, model: nextModel })); - }, [setPage]); + }, [resetCursorPagination]); const queryParams = useMemo(() => ({ page, page_size: pageSize, + before_id: beforeId, ...filters, - }), [filters, page, pageSize]); + }), [beforeId, filters, page, pageSize]); const { platforms, platformName } = usePlatforms(); const platformOptions = [ @@ -262,11 +263,12 @@ export default function UserUsageContent() { function updateFilter(key: string, value: string) { const nextValue = key === 'api_key_id' && value ? Number(value) : value || undefined; setFilters((prev) => ({ ...prev, [key]: nextValue })); - setPage(1); + resetCursorPagination(); } const list = data?.list ?? []; const total = data?.total ?? 0; + const canUseCursor = !isPlaceholderData; const visibleActualCost = customerScope ? (stats?.total_billed_cost ?? 0) : (stats?.total_actual_cost ?? 0); const sharedColumns = useUsageColumns({ customerScope, adminView: false }); @@ -369,7 +371,7 @@ export default function UserUsageContent() { label={t('usage.time_range')} startDate={filters.start_date} onChange={(startDate, endDate) => { - setPage(1); + resetCursorPagination(); setFilters((prev) => ({ ...prev, start_date: startDate, end_date: endDate })); }} /> @@ -460,14 +462,16 @@ export default function UserUsageContent() { emptyTitle={t('common.no_data')} highlightNewRows={autoRefreshEnabled && page === 1} highlightResetKey={JSON.stringify({ ...filters, page, pageSize })} + hasMore={canUseCursor ? data?.has_more : false} isLoading={isLoading} page={page} pageSize={pageSize} rows={list} - setPage={setPage} + setPage={(nextPage) => setPage(nextPage, canUseCursor ? data?.next_cursor : undefined)} setPageSize={setPageSize} suppressHighlight={isPlaceholderData} total={total} + totalExact={canUseCursor ? data?.total_exact : true} /> ); diff --git a/web/src/shared/components/TablePaginationFooter.tsx b/web/src/shared/components/TablePaginationFooter.tsx index 5d05360a..ec653c51 100644 --- a/web/src/shared/components/TablePaginationFooter.tsx +++ b/web/src/shared/components/TablePaginationFooter.tsx @@ -2,25 +2,32 @@ import { ListBox, Pagination, Select } from '@heroui/react'; import { DEFAULT_PAGINATION_PAGE_SIZE_OPTIONS, getPaginationItems } from '../utils/pagination'; interface TablePaginationFooterProps { + hasMore?: boolean; page: number; pageSize?: number; pageSizeOptions?: readonly number[]; setPage: (page: number) => void; setPageSize?: (pageSize: number) => void; total: number; + totalExact?: boolean; totalPages: number; } export function TablePaginationFooter({ + hasMore, page, pageSize, pageSizeOptions = DEFAULT_PAGINATION_PAGE_SIZE_OPTIONS, setPage, setPageSize, total, + totalExact = true, totalPages, }: TablePaginationFooterProps) { - const safeTotalPages = Math.max(totalPages, 1); + const safeTotalPages = totalExact + ? Math.max(totalPages, 1) + : Math.max(totalPages, page + (hasMore ? 1 : 0), 1); + const canGoNext = totalExact ? page < safeTotalPages : !!hasMore; const showPageSize = pageSize != null && setPageSize != null; const selectedPageSize = pageSize == null ? '' : String(pageSize); const pageSizeItems = pageSizeOptions.map((size) => ({ id: String(size), label: String(size) })); @@ -28,7 +35,7 @@ export function TablePaginationFooter({ return ( - + {totalExact ? '共' : '至少'} {total.toLocaleString()}