-
Notifications
You must be signed in to change notification settings - Fork 11.2k
feat: support for tiered billing expressions in the billing system #4409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
91ed4e1
f0589cc
f6c0852
5b03b39
c5405b2
6e3ef48
fbca256
44fc10b
d66311e
d385d7a
35d0704
0220df8
4d2993e
1fe9f6f
5c4ed5b
55b7e48
6bde1a9
8eeae00
3e5f2ee
eab478b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| name: Publish Docker image (nightly) | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - nightly | ||
| workflow_dispatch: | ||
| inputs: | ||
| name: | ||
| description: "reason" | ||
| required: false | ||
|
|
||
| jobs: | ||
| build_single_arch: | ||
| name: Build & push (${{ matrix.arch }}) [native] | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - arch: amd64 | ||
| platform: linux/amd64 | ||
| runner: ubuntu-latest | ||
| - arch: arm64 | ||
| platform: linux/arm64 | ||
| runner: ubuntu-24.04-arm | ||
| runs-on: ${{ matrix.runner }} | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| steps: | ||
| - name: Check out (shallow) | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
|
|
||
| - name: Determine nightly version | ||
| id: version | ||
| run: | | ||
| VERSION="nightly-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" | ||
| echo "$VERSION" > VERSION | ||
| echo "value=$VERSION" >> $GITHUB_OUTPUT | ||
| echo "VERSION=$VERSION" >> $GITHUB_ENV | ||
| echo "Publishing version: $VERSION for ${{ matrix.arch }}" | ||
|
Comment on lines
+37
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Compute the nightly version once per workflow run. Each job recomputes 🐛 Proposed fix jobs:
+ prepare_version:
+ name: Prepare nightly version
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ value: ${{ steps.version.outputs.value }}
+ steps:
+ - name: Check out (shallow)
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Determine nightly version
+ id: version
+ run: |
+ VERSION="nightly-$(date -u +'%Y%m%d')-$(git rev-parse --short HEAD)"
+ echo "value=$VERSION" >> "$GITHUB_OUTPUT"
+
build_single_arch:
name: Build & push (${{ matrix.arch }}) [native]
+ needs: [prepare_version]
@@
- name: Determine nightly version
id: version
run: |
- VERSION="nightly-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)"
+ VERSION="${{ needs.prepare_version.outputs.value }}"
echo "$VERSION" > VERSION
- echo "value=$VERSION" >> $GITHUB_OUTPUT
- echo "VERSION=$VERSION" >> $GITHUB_ENV
+ echo "value=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "Publishing version: $VERSION for ${{ matrix.arch }}"
@@
create_manifests:
name: Create multi-arch manifests (Docker Hub)
- needs: [build_single_arch]
+ needs: [prepare_version, build_single_arch]
runs-on: ubuntu-latest
+ env:
+ VERSION: ${{ needs.prepare_version.outputs.value }}
@@
- - name: Check out (shallow)
- uses: actions/checkout@v4
- with:
- fetch-depth: 1
-
- - name: Determine nightly version
- id: version
- run: |
- VERSION="nightly-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)"
- echo "value=$VERSION" >> $GITHUB_OUTPUT
- echo "VERSION=$VERSION" >> $GITHUB_ENV
-Also applies to: 88-93 🤖 Prompt for AI Agents |
||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Log in to Docker Hub | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| username: ${{ secrets.DOCKERHUB_USERNAME }} | ||
| password: ${{ secrets.DOCKERHUB_TOKEN }} | ||
|
|
||
| - name: Extract metadata (labels) | ||
| id: meta | ||
| uses: docker/metadata-action@v5 | ||
| with: | ||
| images: | | ||
| calciumion/new-api | ||
|
|
||
| - name: Build & push single-arch | ||
| uses: docker/build-push-action@v6 | ||
| with: | ||
| context: . | ||
| platforms: ${{ matrix.platform }} | ||
| push: true | ||
| tags: | | ||
| calciumion/new-api:nightly-${{ matrix.arch }} | ||
| calciumion/new-api:${{ steps.version.outputs.value }}-${{ matrix.arch }} | ||
| labels: ${{ steps.meta.outputs.labels }} | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
| provenance: false | ||
| sbom: false | ||
|
|
||
| create_manifests: | ||
| name: Create multi-arch manifests (Docker Hub) | ||
| needs: [build_single_arch] | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Check out (shallow) | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
|
|
||
| - name: Determine nightly version | ||
| id: version | ||
| run: | | ||
| VERSION="nightly-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" | ||
| echo "value=$VERSION" >> $GITHUB_OUTPUT | ||
| echo "VERSION=$VERSION" >> $GITHUB_ENV | ||
|
|
||
| - name: Log in to Docker Hub | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| username: ${{ secrets.DOCKERHUB_USERNAME }} | ||
| password: ${{ secrets.DOCKERHUB_TOKEN }} | ||
|
|
||
| - name: Create & push manifest (Docker Hub - nightly) | ||
| run: | | ||
| docker buildx imagetools create \ | ||
| -t calciumion/new-api:nightly \ | ||
| calciumion/new-api:nightly-amd64 \ | ||
| calciumion/new-api:nightly-arm64 | ||
|
|
||
| - name: Create & push manifest (Docker Hub - versioned nightly) | ||
| run: | | ||
| docker buildx imagetools create \ | ||
| -t calciumion/new-api:${VERSION} \ | ||
| calciumion/new-api:${VERSION}-amd64 \ | ||
| calciumion/new-api:${VERSION}-arm64 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,5 +29,6 @@ data/ | |
| .gomodcache/ | ||
| .gocache-temp | ||
| .gopath | ||
|
|
||
| token_estimator_test.go | ||
| .test | ||
| token_estimator_test.go | ||
| skills-lock.json | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,7 @@ import ( | |||||||||||||||||||||||||||||
| "github.com/QuantumNous/new-api/dto" | ||||||||||||||||||||||||||||||
| "github.com/QuantumNous/new-api/middleware" | ||||||||||||||||||||||||||||||
| "github.com/QuantumNous/new-api/model" | ||||||||||||||||||||||||||||||
| "github.com/QuantumNous/new-api/pkg/billingexpr" | ||||||||||||||||||||||||||||||
| "github.com/QuantumNous/new-api/relay" | ||||||||||||||||||||||||||||||
| relaycommon "github.com/QuantumNous/new-api/relay/common" | ||||||||||||||||||||||||||||||
| relayconstant "github.com/QuantumNous/new-api/relay/constant" | ||||||||||||||||||||||||||||||
|
|
@@ -233,6 +234,15 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, | |||||||||||||||||||||||||||||
| info.IsChannelTest = true | ||||||||||||||||||||||||||||||
| info.InitChannelMeta(c) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| err = attachTestBillingRequestInput(info, request) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return testResult{ | ||||||||||||||||||||||||||||||
| context: c, | ||||||||||||||||||||||||||||||
| localErr: err, | ||||||||||||||||||||||||||||||
| newAPIError: types.NewError(err, types.ErrorCodeJsonMarshalFailed), | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| err = helper.ModelMappedHelper(c, info, request) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return testResult{ | ||||||||||||||||||||||||||||||
|
|
@@ -469,21 +479,11 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| info.SetEstimatePromptTokens(usage.PromptTokens) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| quota := 0 | ||||||||||||||||||||||||||||||
| if !priceData.UsePrice { | ||||||||||||||||||||||||||||||
| quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio)) | ||||||||||||||||||||||||||||||
| quota = int(math.Round(float64(quota) * priceData.ModelRatio)) | ||||||||||||||||||||||||||||||
| if priceData.ModelRatio != 0 && quota <= 0 { | ||||||||||||||||||||||||||||||
| quota = 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| quota = int(priceData.ModelPrice * common.QuotaPerUnit) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| quota, tieredResult := settleTestQuota(info, priceData, usage) | ||||||||||||||||||||||||||||||
| tok := time.Now() | ||||||||||||||||||||||||||||||
| milliseconds := tok.Sub(tik).Milliseconds() | ||||||||||||||||||||||||||||||
| consumedTime := float64(milliseconds) / 1000.0 | ||||||||||||||||||||||||||||||
| other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatioInfo.GroupRatio, priceData.CompletionRatio, | ||||||||||||||||||||||||||||||
| usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice, priceData.GroupRatioInfo.GroupSpecialRatio) | ||||||||||||||||||||||||||||||
| other := buildTestLogOther(c, info, priceData, usage, tieredResult) | ||||||||||||||||||||||||||||||
| model.RecordConsumeLog(c, 1, model.RecordConsumeLogParams{ | ||||||||||||||||||||||||||||||
| ChannelId: channel.Id, | ||||||||||||||||||||||||||||||
| PromptTokens: usage.PromptTokens, | ||||||||||||||||||||||||||||||
|
|
@@ -505,6 +505,50 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func attachTestBillingRequestInput(info *relaycommon.RelayInfo, request dto.Request) error { | ||||||||||||||||||||||||||||||
| if info == nil { | ||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| input, err := helper.BuildBillingExprRequestInputFromRequest(request, info.RequestHeaders) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| info.BillingRequestInput = &input | ||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func settleTestQuota(info *relaycommon.RelayInfo, priceData types.PriceData, usage *dto.Usage) (int, *billingexpr.TieredResult) { | ||||||||||||||||||||||||||||||
| if usage != nil && info != nil && info.TieredBillingSnapshot != nil { | ||||||||||||||||||||||||||||||
| isClaudeUsageSemantic := usage.UsageSemantic == "anthropic" || info.GetFinalRequestRelayFormat() == types.RelayFormatClaude | ||||||||||||||||||||||||||||||
| usedVars := billingexpr.UsedVars(info.TieredBillingSnapshot.ExprString) | ||||||||||||||||||||||||||||||
| if ok, quota, result := service.TryTieredSettle(info, service.BuildTieredTokenParams(usage, isClaudeUsageSemantic, usedVars)); ok { | ||||||||||||||||||||||||||||||
| return quota, result | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| quota := 0 | ||||||||||||||||||||||||||||||
| if !priceData.UsePrice { | ||||||||||||||||||||||||||||||
| quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio)) | ||||||||||||||||||||||||||||||
| quota = int(math.Round(float64(quota) * priceData.ModelRatio)) | ||||||||||||||||||||||||||||||
| if priceData.ModelRatio != 0 && quota <= 0 { | ||||||||||||||||||||||||||||||
| quota = 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return quota, nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return int(priceData.ModelPrice * common.QuotaPerUnit), nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+530
to
+541
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The legacy path ( 🔧 Proposed fix- return int(priceData.ModelPrice * common.QuotaPerUnit), nil
+ return int(priceData.ModelPrice * common.QuotaPerUnit * priceData.GroupRatioInfo.GroupRatio), nilConsider applying the same 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func buildTestLogOther(c *gin.Context, info *relaycommon.RelayInfo, priceData types.PriceData, usage *dto.Usage, tieredResult *billingexpr.TieredResult) map[string]interface{} { | ||||||||||||||||||||||||||||||
| other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatioInfo.GroupRatio, priceData.CompletionRatio, | ||||||||||||||||||||||||||||||
| usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice, priceData.GroupRatioInfo.GroupSpecialRatio) | ||||||||||||||||||||||||||||||
| if tieredResult != nil { | ||||||||||||||||||||||||||||||
| service.InjectTieredBillingInfo(other, info, tieredResult) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+543
to
+548
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve tiered log metadata when settlement falls back.
Proposed fix- if tieredResult != nil {
+ if info != nil &&
+ info.TieredBillingSnapshot != nil &&
+ info.TieredBillingSnapshot.BillingMode == "tiered_expr" {
service.InjectTieredBillingInfo(other, info, tieredResult)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| return other | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func coerceTestUsage(usageAny any, isStream bool, estimatePromptTokens int) (*dto.Usage, error) { | ||||||||||||||||||||||||||||||
| switch u := usageAny.(type) { | ||||||||||||||||||||||||||||||
| case *dto.Usage: | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid publishing
nightlyfrom mutable per-arch tags during overlapping runs.Two pushes to
nightlycan interleave: one run may createcalciumion/new-api:nightlyfromnightly-amd64andnightly-arm64tags produced by different commits, or an older run can finish last and movenightlybackward.🐛 Proposed fix
on: push: branches: - nightly workflow_dispatch: inputs: name: description: "reason" required: false + +concurrency: + group: docker-nightly-${{ github.ref }} + cancel-in-progress: true @@ - name: Create & push manifest (Docker Hub - nightly) run: | docker buildx imagetools create \ -t calciumion/new-api:nightly \ - calciumion/new-api:nightly-amd64 \ - calciumion/new-api:nightly-arm64 + calciumion/new-api:${VERSION}-amd64 \ + calciumion/new-api:${VERSION}-arm64Also applies to: 101-106
🤖 Prompt for AI Agents