package plugin import ( "context" "encoding/json" "fmt" "net/http" "strings" "sync" "sync/atomic" "time" managedaccess "billing/internal/access" "billing/internal/collection" "billing/internal/modelcatalog" "billing/internal/pricing" "billing/internal/repository" ) type App struct { mu sync.RWMutex priceMu sync.Mutex config Config usage *collection.Service store *repository.SQLiteUsageRepository prices map[string]pricing.Policy catalog *modelcatalog.Manager host HostCaller closed bool seen atomic.Uint64 pending sync.Map scopes sync.Map observedUpstreams sync.Map } func NewApp() *App { return &App{config: defaultConfig(), prices: make(map[string]pricing.Policy)} } func (a *App) HandleMethod(method string, request []byte) (response []byte, err error) { defer func() { if recovered := recover(); recovered != nil { response = nil err = fmt.Errorf("插件处理 %s 时发生异常: %v", method, recovered) } }() switch method { case MethodPluginRegister, MethodPluginReconfigure: return a.configure(request) case MethodUsageHandle: return a.handleUsage(request) case MethodFrontendIdentifier: return a.frontendIdentifier() case MethodFrontendAuthenticate: return a.authenticate(request) case MethodSchedulerPick: return a.pickScheduler(request) case MethodRequestBefore: return a.interceptRequest(request, false) case MethodRequestAfter: return a.interceptRequest(request, true) case MethodRequestComplete: return a.handleRequestComplete(request) case MethodManagementRegister: return OKEnvelope(managementRegistration()) case MethodManagementHandle: return a.handleManagement(request) case MethodPluginShutdown: a.Shutdown() return OKEnvelope(struct{}{}) default: return ErrorEnvelope("unknown_method", "不支持的插件方法: "+method, http.StatusNotFound), nil } } func (a *App) configure(raw []byte) ([]byte, error) { var req LifecycleRequest if len(raw) > 0 { if err := json.Unmarshal(raw, &req); err != nil { return nil, fmt.Errorf("解析生命周期请求: %w", err) } } if req.SchemaVersion == 0 { req.SchemaVersion = 1 } negotiated := min(req.SchemaVersion, SchemaVersion) cfg, err := decodeConfig(req.ConfigYAML) if err != nil { return nil, err } usageRepository, err := repository.OpenSQLiteUsage(cfg.DatabasePath) if err != nil { return nil, err } if _, err := usageRepository.BootstrapManagedKey(context.Background(), cfg.BootstrapName, cfg.BootstrapKey); err != nil { _ = usageRepository.Close() return nil, err } nextUsage := collection.NewService(usageRepository) policies, err := usageRepository.ListPrices(context.Background()) if err != nil { _ = nextUsage.Close() return nil, err } nextPrices := make(map[string]pricing.Policy, len(policies)) for _, policy := range policies { nextPrices[normalizeModelName(policy.Model)] = policy } nextCatalog := modelcatalog.NewManager(cfg.CatalogPath, cfg.ModelsDevURL) a.mu.RLock() releaseStaleAdmissions := a.usage == nil || a.config.DatabasePath != cfg.DatabasePath a.mu.RUnlock() if releaseStaleAdmissions { if err := usageRepository.ReleaseStaleAdmissions(context.Background(), time.Now().UTC()); err != nil { _ = nextUsage.Close() return nil, fmt.Errorf("释放遗留并发占用: %w", err) } } a.mu.Lock() defer a.mu.Unlock() if a.closed { _ = nextUsage.Close() return nil, fmt.Errorf("插件已经关闭") } previousUsage := a.usage a.config = cfg a.usage = nextUsage a.store = usageRepository a.prices = nextPrices a.catalog = nextCatalog if previousUsage != nil { _ = previousUsage.Close() } return OKEnvelope(registration(negotiated)) } func registration(schemaVersion uint32) Registration { return Registration{ SchemaVersion: schemaVersion, Metadata: Metadata{ Name: PluginName, Version: Version, Author: "billing", GitHubRepository: "https://git.pchuan.top/agent/cpa-plugin", ConfigFields: []ConfigField{ {Name: "enabled", Type: "boolean", Description: "启用 CPA 扩展。"}, {Name: "codex_only", Type: "boolean", Description: "只接收 Codex/OpenAI 模型的用量事件。"}, {Name: "database_path", Type: "string", Description: "SQLite 数据库文件路径。"}, {Name: "bootstrap_name", Type: "string", Description: "首次启动时现有 Key 的名称。"}, {Name: "bootstrap_key", Type: "string", Description: "首次启动时导入的现有下游 Key。"}, {Name: "models_dev_url", Type: "string", Description: "models.dev 合并价格目录地址。"}, {Name: "models_dev_cache_path", Type: "string", Description: "规范化价格目录缓存文件;默认与数据库同目录。"}, }, }, Capabilities: Capabilities{ FrontendAuthProvider: true, FrontendAuthProviderExclusive: true, Scheduler: true, RequestInterceptor: true, RequestLifecyclePlugin: true, UsagePlugin: true, ManagementAPI: true, }, } } func (a *App) handleUsage(raw []byte) ([]byte, error) { var record UsageRecord if err := json.Unmarshal(raw, &record); err != nil { return nil, fmt.Errorf("解析用量事件: %w", err) } a.mu.RLock() defer a.mu.RUnlock() if a.closed { return nil, fmt.Errorf("插件已经关闭") } if a.config.accepts(record) { if a.usage == nil { return nil, fmt.Errorf("用量数据库尚未初始化") } // CPA wire 类型只存在于适配层,采集模块接收与协议无关的观察值。 observed := collection.Record{ RequestedAt: record.RequestedAt, APIKey: record.APIKey, AuthID: record.AuthID, AuthIndex: record.AuthIndex, AuthType: record.AuthType, Model: record.Model, ReasoningEffort: record.ReasoningEffort, ServiceTier: record.ServiceTier, Failed: record.Failed, ExecutorType: record.ExecutorType, InputTokens: record.Detail.InputTokens, OutputTokens: record.Detail.OutputTokens, TotalTokens: record.Detail.TotalTokens, CachedTokens: record.Detail.CachedTokens, CacheReadTokens: record.Detail.CacheReadTokens, CacheWriteTokens: record.Detail.CacheCreationTokens, ReasoningTokens: record.Detail.ReasoningTokens, TTFT: record.TTFT, Latency: record.Latency, } if key, keyErr := a.resolveUsageManagedKey(a.store, record.APIKey); keyErr == nil { observed.ManagedKeyID = key.ID observed.KeyAlias = key.Name } if policy, found := a.prices[normalizeModelName(record.Model)]; found { cost, calculateErr := pricing.Calculate(policy, pricing.Usage{ InputTokens: record.Detail.InputTokens, CacheReadTokens: record.Detail.CacheReadTokens, CacheWriteTokens: record.Detail.CacheCreationTokens, OutputTokens: record.Detail.OutputTokens, ServiceTier: record.ServiceTier, }) if calculateErr == nil { observed.CostMicros = &cost.CostMicros observed.PriceTier = cost.PriceTier observed.FastRequested = cost.FastRequested observed.FastPricingApplied = cost.FastApplied observed.PriceMultiplierNumerator = cost.MultiplierNumerator observed.PriceMultiplierDenominator = cost.MultiplierDenominator } } if err := a.usage.Observe(context.Background(), observed); err != nil { return nil, err } a.seen.Add(1) } return OKEnvelope(struct{}{}) } func (a *App) resolveUsageManagedKey(store *repository.SQLiteUsageRepository, reference string) (managedaccess.ManagedKey, error) { if keyID, ok := a.scopes.Load(strings.TrimSpace(reference)); ok { if id, valid := keyID.(string); valid { if key, err := store.ManagedKeyByID(context.Background(), id); err == nil { return key, nil } } } if key, err := store.ManagedKeyByReference(context.Background(), reference); err == nil { return key, nil } return store.ManagedKeyByCredential(context.Background(), reference) } func (a *App) handleRequestComplete(raw []byte) ([]byte, error) { var completion RequestCompletion if err := json.Unmarshal(raw, &completion); err != nil { return nil, fmt.Errorf("解析请求终态: %w", err) } a.mu.RLock() defer a.mu.RUnlock() if a.closed { return nil, fmt.Errorf("插件已经关闭") } if a.usage == nil { return nil, fmt.Errorf("用量数据库尚未初始化") } pendingManagedKeyID := "" if pending, ok := a.pending.LoadAndDelete(completion.RequestID); ok { pendingManagedKeyID, _ = pending.(string) } if err := a.store.CompleteBillingAdmission(context.Background(), completion.RequestID, completion.CompletedAt); err != nil { return nil, fmt.Errorf("释放并发占用: %w", err) } endpoint, _ := completion.Metadata["request_path"].(string) managedKeyID := metadataString(completion.Metadata, callerScopeMetadata) if key, keyErr := a.store.ManagedKeyByReference(context.Background(), managedKeyID); keyErr == nil { managedKeyID = key.ID } if managedKeyID == "" { managedKeyID = pendingManagedKeyID } if err := a.usage.ObserveRequest(context.Background(), collection.RequestRecord{ ManagedKeyID: managedKeyID, RequestID: completion.RequestID, TraceID: completion.TraceID, RequestedAt: completion.StartedAt, CompletedAt: completion.CompletedAt, Model: completion.Model, RequestedModel: completion.RequestedModel, SourceFormat: completion.SourceFormat, Stream: completion.Stream, Outcome: string(completion.Outcome), StatusCode: completion.StatusCode, Error: completion.Error, Endpoint: strings.TrimSpace(endpoint), }); err != nil { return nil, err } return OKEnvelope(struct{}{}) } func isCompactEndpoint(endpoint string) bool { path := strings.ToLower(strings.TrimSpace(endpoint)) if fields := strings.Fields(path); len(fields) > 1 { path = fields[len(fields)-1] } if query := strings.IndexByte(path, '?'); query >= 0 { path = path[:query] } return strings.HasSuffix(strings.TrimRight(path, "/"), "/responses/compact") } func (a *App) Seen() uint64 { return a.seen.Load() } func (a *App) Shutdown() { a.mu.Lock() defer a.mu.Unlock() if a.closed { return } a.closed = true if a.usage != nil { _ = a.usage.Close() a.usage = nil a.store = nil a.catalog = nil } }