Commit Graph
1940 Commits
Author SHA1 Message Date
erio 90d99f77e4 feat(channel): improve cache strategy and add restriction logging
- Change channel cache TTL from 60s to 10min (reduce unnecessary DB queries)
- Actively rebuild cache after CRUD instead of lazy invalidation
- Add slog.Warn logging for channel pricing restriction blocks (4 places)
2026-04-03 13:54:18 +08:00
erio 2e9c151e8f style: fix gofmt struct tag alignment 2026-04-03 02:09:49 +08:00
erio fa430746ac feat(redeem): support negative values for refund/deduction
Allow redeem codes with negative values to enable refund scenarios:
- Balance: negative value deducts balance (clamped to 0, never negative)
- Concurrency: negative value reduces concurrency (clamped to 0)
- Subscription: negative validity_days reduces remaining days; if
  remaining days <= 0, the subscription is canceled (set to expired)

All deductions generate standard redeem code records for audit trail.
2026-04-03 01:51:14 +08:00
erio 89825646bc fix: channel cache fail-close, group conflict check across pages, status toggle stale data
- GetGroupPlatforms failure now stores error-TTL cache and returns error (fail-close)
- Frontend group-to-channel conflict map loads all channels instead of current page only
- Toggle channel status reloads list when active filter would hide the changed item
2026-04-02 23:47:37 +08:00
erio 259506b429 fix: sticky session upstream restriction bypass + 429 over-aggressive credits exhaustion
1. Sticky session now checks upstream channel restriction:
   - Add isStickyAccountUpstreamRestricted() helper
   - Apply to both routing sticky (selectAccountForModelWithPlatform)
     and normal sticky (selectAccountWithMixedScheduling)

2. shouldMarkCreditsExhausted: only mark on 429 with credits keywords:
   - Node-level rate limiting / transient 429 no longer triggers 5h circuit break
   - Extract containsCreditsExhaustedKeyword() to unify 429 and 403 paths
2026-04-02 20:57:13 +08:00
erio 1a27a4dd4c fix: resolve 5 audit findings in channel/credits/scheduling
P0-1: Credits degraded response retry + fail-open
- Add isAntigravityDegradedResponse() to detect transient API failures
- Retry up to 3 times with exponential backoff (500ms/1s/2s)
- Invalidate singleflight cache between retries
- Fail-open after exhausting retries instead of 5h circuit break

P1-1: Fix channel restriction pre-check timing conflict
- Swap checkClaudeCodeRestriction before checkChannelPricingRestriction
- Ensures channel restriction is checked against final fallback groupID

P1-2: Add interval pricing validation (frontend + backend)
- Backend: ValidateIntervals() with boundary, price, overlap checks
- Frontend: validateIntervals() with Chinese error messages
- Rules: MinTokens>=0, MaxTokens>MinTokens, prices>=0, no overlap

P2: Fix cross-platform same-model pricing/mapping override
- Store cache keys using original platform instead of group platform
- Lookup across matching platforms (antigravity→anthropic→gemini)
- Prevents anthropic/gemini same-name models from overwriting each other
2026-04-02 20:28:04 +08:00
erio 3e80807e74 test: add unit tests for channel pricing restriction in scheduling phase
20 test cases covering:
- billingModelForRestriction: 4 cases (requested/channel_mapped/upstream/empty)
- resolveAccountUpstreamModel: 3 cases (antigravity/unsupported/non-antigravity)
- checkChannelPricingRestriction: 10 cases (nil guards, 3 billing sources,
  RestrictModels disabled, no channel)
- isUpstreamModelRestrictedByChannel: 3 cases (restricted/allowed/unsupported)
2026-04-02 13:40:36 +08:00
erio 6229773ea0 fix: address review findings for channel restriction refactoring
- Fix 7 stale comments still mentioning "限制检查" in handlers/services
- Make billingModelForRestriction explicitly list channel_mapped case
- Add slog.Warn for error swallowing in ResolveChannelMapping and
  needsUpstreamChannelRestrictionCheck
- Document sticky session upstream check exemption
2026-04-02 13:36:58 +08:00
erio 513e653a4e refactor: move channel model restriction from handler to scheduling phase
Move the model pricing restriction check from 8 handler entry points
to the account scheduling phase (SelectAccountForModelWithExclusions /
SelectAccountWithLoadAwareness), aligning restriction with billing:

- requested: check original request model against pricing list
- channel_mapped: check channel-mapped model against pricing list
- upstream: per-account check using account-mapped model

Handler layer now only resolves channel mapping (no restriction).
Scheduling layer performs pre-check for requested/channel_mapped,
and per-account filtering for upstream billing source.
2026-04-02 13:24:30 +08:00
erio 333ddfdb21 refactor: extract helpers to reduce duplication and function length in gateway billing
- Extract resolveChannelPricing to DRY the resolver pattern shared by calculateImageCost/calculateTokenCost
- Remove unnecessary IIFE wrapper and pass accountRateMultiplier as parameter
- Extract resolveBillingMode, resolveMediaType, optionalSubscriptionID to simplify buildRecordUsageLog (104→65 lines)
- Extract shouldDeductAPIKeyQuota/shouldUpdateRateLimits/shouldUpdateAccountQuota methods on postUsageBillingParams to unify duplicated billing conditions
2026-04-02 03:28:52 +08:00
erio 75374d9e0d refactor: merge RecordUsage and RecordUsageWithLongContext into shared core
- Extract recordUsageCore with recordUsageOpts for parameterized differences
- RecordUsage (276 lines) → thin wrapper (~40 lines)
- RecordUsageWithLongContext (251 lines) → thin wrapper (~20 lines)
- Split billing logic into calculateSoraMediaCost, calculateImageCost,
  calculateTokenCost sub-functions
- Extract buildRecordUsageLog for usage log construction
- Net reduction: -79 lines, eliminated ~170 lines of duplication
2026-04-02 02:45:43 +08:00
erio a1825024b4 refactor: split channelToResponse into pricingToResponse + intervalToResponse 2026-04-02 02:35:11 +08:00
erio f968e1bd95 refactor: extract computeTokenBreakdown to deduplicate billing logic
- calculateTokenCost reduced from 80 to 15 lines
- calculateCostInternal reduced from 91 to 15 lines
- Shared logic in computeTokenBreakdown + computeCacheCreationCost
- Unified rateMultiplier <= 0 protection in both paths
2026-04-02 02:33:05 +08:00
erio ed01e0dcb0 refactor: use structured error responses in channel handler
Replace response.BadRequest with response.ErrorFrom + infraerrors.BadRequest
to provide machine-readable reason codes (VALIDATION_ERROR, INVALID_CHANNEL_ID,
MISSING_PARAMETER) for frontend i18n support.
2026-04-02 02:32:39 +08:00
erio a745a83521 refactor: split buildCache into sub-functions, reduce nesting 5→2
- Extract newEmptyChannelCache() factory to deduplicate map init
- Extract expandPricingToCache() for model pricing expansion
- Extract expandMappingToCache() for model mapping expansion
- buildCache reduced from 110 to 50 lines
2026-04-02 02:28:32 +08:00
erio 198010f114 refactor: replace magic strings with named constants
- PricingSourceChannel/LiteLLM/Fallback for resolver source
- MediaTypeImage/Video/Prompt for result.MediaType
- Reuse BillingModeToken/BillingModeImage for billing mode
- Reuse BillingModelSourceChannelMapped/PlatformAnthropic in handler
2026-04-02 02:22:15 +08:00
erio 95d9b2d1d0 fix: billing mode display follows cost calculation result
Instead of hardcoding BillingMode="image" when ImageCount>0,
let cost.BillingMode (set by CalculateCostUnified/CalculateImageCost)
take priority. This ensures channel token pricing shows "token" mode.
2026-04-02 01:34:19 +08:00
erio ab74dcbf9d debug: add image billing resolve logging for troubleshooting 2026-04-02 01:09:49 +08:00
erio e9586ac085 fix: validate prices must be >= 0, remove debug logs 2026-04-02 00:52:19 +08:00
erio 5fb1340681 chore: remove debug logs for candidatesTokensDetails investigation 2026-04-02 00:48:36 +08:00
erio 435f88d984 debug: print v1internal raw payload for non-stream path 2026-04-02 00:29:44 +08:00
erio 194bde4651 debug: check candidatesTokensDetails in v1internal wrapper 2026-04-02 00:24:35 +08:00
erio e772c6ba2d debug: log candidatesTokensDetails presence in extractGeminiUsage 2026-04-02 00:17:38 +08:00
erio 4a24921ca9 feat: channel token pricing takes priority over per-image billing
When ImageCount > 0, check if channel has token pricing configured:
- YES (source=channel, mode=token) → use token billing with image_output_tokens
- NO → fall back to CalculateImageCost (original per-image billing)

This allows channels to configure $/MTok pricing for image generation
models while maintaining backward compatibility for setups without
channel pricing.
2026-04-01 23:29:01 +08:00
erio 5f48e903bf fix: address audit findings - cache sync, validation, consistency
- clearCreditsExhausted: sync Redis scheduler cache after DB update
- Image billing mode UI: write to per_request_price instead of image_output_price
- OpenAI RecordUsage: use BillingModelSourceRequested constant, add s.cfg nil guard
- Fix i18n key path: admin.channels.perRequestPriceRequired → admin.channels.form.perRequestPriceRequired
2026-04-01 23:13:58 +08:00
erio bace10ff61 test: add unit tests for channel platform matching, interval validation, credits check
- TestIsPlatformPricingMatch: 12 cases covering all platform combinations
- TestMatchingPlatforms: 4 cases for platform expansion
- TestGetChannelModelPricing_AntigravityCrossPlatform: antigravity sees anthropic pricing
- TestGetChannelModelPricing_AnthropicCannotSeeAntigravityPricing: no reverse leakage
- TestResolveChannelMapping_AntigravityCrossPlatform: antigravity uses anthropic mapping
- TestFilterValidIntervals: 8 cases for empty interval filtering
- TestHasEnoughCredits: 10 cases for credits balance threshold logic
- Extract hasEnoughCredits() pure function for testability
2026-04-01 22:41:39 +08:00
erio 8b179af2e4 fix: validate empty intervals + antigravity platform pricing match
- Backend: reject intervals with all-null price fields on save
- Backend: filterValidIntervals skips empty intervals in pricing resolver
- Frontend: red border + asterisk on empty interval rows
- Backend: antigravity groups now match anthropic/gemini channel pricing
2026-04-01 22:03:00 +08:00
erio 4d8683a115 fix: antigravity groups now match anthropic/gemini channel pricing
Antigravity platform serves both Claude and Gemini models, but channel
pricing/mapping is configured under Anthropic/Gemini tabs. The cache
builder was using strict platform equality, causing antigravity groups
to miss all channel pricing entries, resulting in $0 billing.

Add isPlatformPricingMatch() to treat antigravity as superset of
anthropic+gemini for pricing and mapping cache indexing.
2026-04-01 21:47:47 +08:00
erio eff0ceb14a fix: add cost nil guard to Anthropic/Antigravity RecordUsage paths
- Apply same nil-pointer protection as OpenAI path
- Remove unused accessToken/proxyURL params from checkAccountCredits
2026-04-01 17:13:31 +08:00
erio 327a4fb248 fix: golangci-lint test assertion and gofmt 2026-04-01 16:39:38 +08:00
erio 6af5b9ff9c fix: resolve golangci-lint issues
- Fix errcheck: defer rows.Close() with nolint
- Fix errcheck: type assertion with ok check in channel cache
- Fix staticcheck ST1005: lowercase error string
- Fix staticcheck SA5011: nil check cost before use in openai gateway
- Fix gofmt: format chatcompletions_to_responses.go
2026-04-01 16:30:47 +08:00
erio a1216b8839 feat: image output token billing, channel-mapped billing source, credits balance precheck
- Parse candidatesTokensDetails from Gemini API to separate image/text output tokens
- Add image_output_tokens and image_output_cost to usage_log (migration 089)
- Support per-image-token pricing via output_cost_per_image_token from model pricing data
- Channel pricing ImageOutputPrice override works in token billing mode
- Auto-fill image_output_price in channel pricing form from model defaults
- Add "channel_mapped" billing model source as new default (migration 088)
- Bills by model name after channel mapping, before account mapping
- Fix channel cache error TTL sign error (115s → 5s)
- Fix Update channel only invalidating new groups, not removed groups
- Fix frontend model_mapping clearing sending undefined instead of {}
- Credits balance precheck via shared AccountUsageService cache before injection
- Skip credits injection for accounts with insufficient balance
- Don't mark credits exhausted for "exhausted your capacity on this model" 429s
2026-04-01 15:08:57 +08:00
erio 47266e03e6 fix(antigravity): 积分注入后仍 429 直接标记 credits exhausted
旧逻辑中 shouldMarkCreditsExhausted 因 parseAntigravitySmartRetryInfo
能解析出 retry info 就 return false,导致 free 账号注入积分后上游返回
429 也永远不标记 exhausted,形成无限重试死循环。

修复:429 直接标记 exhausted,其他 4xx 保留关键词匹配(如 403 积分不足)。
2026-04-01 02:40:52 +08:00
erio 7abc6e02f4 feat(channel): 渠道管理全链路集成 — 模型映射、定价、限制、用量统计
- 渠道模型映射:支持精确匹配和通配符映射,按平台隔离
- 渠道模型定价:支持 token/按次/图片三种计费模式,区间分层定价
- 模型限制:渠道可限制仅允许定价列表中的模型
- 计费模型来源:支持 requested/upstream 两种计费模型选择
- 用量统计:usage_logs 新增 channel_id/model_mapping_chain/billing_tier/billing_mode 字段
- Dashboard 支持 model_source 维度(requested/upstream/mapping)查看模型统计
- 全部 gateway handler 统一接入 ResolveChannelMappingAndRestrict
- 修复测试:同步 SoraGenerationRepository 接口、SQL INSERT 参数、scan 字段
2026-04-01 01:51:19 +08:00
erio 9df5d90e78 fix(channel): 模型限制以原始请求模型检查定价列表,而非映射后模型
开启 restrict_models 时,应用原始模型名查定价列表;
定价列表未命中即拒绝,不因通配符映射而绕过限制。
2026-03-31 20:38:02 +08:00
erio adcf6328b4 fix(openai): 非 OAuth 账号 Chat Completions 也提取 system 消息并注入 instructions
GPT-5/Codex 等模型要求请求中必须包含 instructions 字段。
之前 extractSystemMessagesFromInput + applyInstructions 仅在
OAuth 路径执行,API Key 账号走 Chat Completions 时 system 消息
保留在 input 中但缺少 instructions,导致上游返回 400。

同时在 ChatCompletionsRequest 和 ResponsesRequest 中增加
Instructions 字段,支持客户端直接传入 instructions 透传到上游。
2026-03-31 20:11:39 +08:00
erio 72b9d198a1 feat(channel): 模型映射源支持通配符匹配
与定价通配符一致,映射源支持 * 后缀通配符(最长前缀优先):
- `*` 匹配所有模型
- `claude-*` 匹配 claude- 开头的模型
- 精确匹配优先于通配符
2026-03-31 19:32:14 +08:00
erio 3840dc8787 fix(channel): 修复 invalidateCache 存入 typed nil 导致 loadCache panic
invalidateCache 存入 (*channelCache)(nil),类型断言 ok=true 但
指针为 nil,后续 cached.loadedAt 导致 nil pointer dereference。
在 loadCache 双重检查处增加 cached != nil 防御。
2026-03-31 18:37:30 +08:00
erio bd17bde19c Merge branch 'feature/channel' into release/custom-0.1.105 2026-03-31 17:23:31 +08:00
erio d058e5c112 fix(channel): 全平台渠道映射覆盖 + 公共函数抽取 + 死代码清理
- 4个缺失handler入口添加渠道映射+限制检查(ChatCompletions/Responses/Gemini)
- 模型限制错误信息优化,区分"模型不可用"和"无账号"
- OpenAI RecordUsage RequestedModel 改用 OriginalModel
- ResolveChannelMappingAndRestrict/ReplaceModelInBody 抽取到 ChannelService 消除跨service重复
- validateNoDuplicateModels 按 platform:model 去重
- 删除 Channel.ResolveMappedModel 死代码和 CalculateCostWithChannel Deprecated方法
- 移除冗余nil检查,抽取 validatePricingBillingMode 公共校验
2026-03-31 15:26:20 +08:00
erio b4514501dd Merge remote-tracking branch 'origin/feature/channel' into release/custom-0.1.105 2026-03-31 02:11:52 +08:00
erio d1791f43f8 refactor(channel): 抽取渠道映射公共函数 + OpenAI映射到body + 空响应修复 + 清理日志
- 抽取 ResolveChannelMappingAndRestrict 统一入口(5处→1个方法)
- 抽取 BuildModelMappingChain 到 ChannelMappingResult 方法(5处→1行调用)
- OpenAI 三入口 Forward 前应用渠道映射到请求体
- OpenAI Responses/Messages 限制检查添加错误响应
- 清理前端 3 处 console.log 调试日志
2026-03-31 02:11:24 +08:00
erio 8741864b79 Merge remote-tracking branch 'origin/feature/channel' into release/custom-0.1.105 2026-03-31 01:34:37 +08:00
erio 946180ef0d fix(channel): 模型限制用映射后模型检查 + 平台开关保留配置不删除
- OpenAI 网关三处 IsModelRestricted 改用 channelMapping.MappedModel
- 前端平台勾选改为 enabled 开关,取消勾选不清空配置数据
- formToAPI/校验只处理 enabled 的平台
2026-03-31 01:34:16 +08:00
erio f27f1254ae Merge remote-tracking branch 'origin/feature/channel' into release/custom-0.1.105 2026-03-31 00:24:22 +08:00
erio a65a8bcd93 feat(channel): 通配符定价匹配 + OpenAI BillingModelSource + 按次价格校验 + 用户端计费模式展示
- 定价查找支持通配符(suffix *),最长前缀优先匹配
- 模型限制(restrict_models)同样支持通配符匹配
- OpenAI 网关接入渠道映射/BillingModelSource/模型限制
- 按次/图片计费模式创建时强制要求价格或层级(前后端)
- 用户使用记录列表增加计费模式 badge 列
2026-03-31 00:23:45 +08:00
erio 7347916f09 Merge remote-tracking branch 'origin/feature/channel' into release/custom-0.1.105 2026-03-30 23:05:04 +08:00
erio 08593ea3e4 fix(billing): 按次计费回退到默认 PerRequestPrice
ResolvedPricing 新增 DefaultPerRequestPrice,当无层级匹配时使用渠道的默认按次价格
2026-03-30 23:04:48 +08:00
erio 9876c05efc merge: feature/channel → release (v0.1.105.15) — 计费迁移到 CalculateCostUnified 2026-03-30 22:59:15 +08:00
erio f0d5163973 feat(billing): 网关计费迁移到 CalculateCostUnified + 模型限制错误统一
- GatewayService/OpenAIGatewayService 注入 ModelPricingResolver
- RecordUsage 从旧路径迁移到 CalculateCostUnified(支持 per_request/image 模式)
- 无渠道时自动回退旧路径,保持原有行为
- 长上下文双倍计费仅在无渠道定价时生效
- CostBreakdown 新增 BillingMode 字段,使用日志记录实际计费模式
- 模型限制错误改为与"无可用账号"相同的 503 响应
2026-03-30 22:58:28 +08:00