Files
easyai-ai-gateway/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/model-billing-configuration-and-estimation.md
T
chengcheng e3dfe8162b fix(billing): 视频时长按实际秒数线性计费
将五秒基础价按 duration / 5 比例结算,保留 provider 返回的小数时长,避免六秒视频被按两个完整单位收费。

影响:所有使用 5s 视频基础价的模型,quantity 与 durationUnitCount 允许小数。新增迁移同步现存规则的旧 ceil 公式元数据。

验证:go vet ./...;pnpm lint;pnpm test;pnpm build;./tests/ci/migrations-test.sh
2026-07-21 13:32:23 +08:00

11 KiB
Raw Blame History

Model Billing Configuration and Estimate Reconciliation

Contents

  • Safety and Identity
  • Effective Billing Layers
  • Billing Configuration Workflow
  • Price Estimate Requests
  • Estimate Reconciliation
  • Task and Wallet Charge Verification
  • Expected Differences and Troubleshooting

Safety and Identity

  • Use an administrator JWT with manager permission for pricing, base-model, platform, platform-model, or user-group writes. Do not use an sk-* API key for management APIs.
  • Call POST /api/v1/pricing/estimate with the same user JWT or API key that will execute the real request. The subject controls access rules, candidate visibility, API-key scopes, and user-group billing discount.
  • The estimate endpoint is read-only for tasks and wallets: it selects and preprocesses a candidate and returns simulated billing lines, but it does not create a task, freeze balance, or debit the wallet.
  • A request with runMode: "simulation" is different: it enters the task execution and billing path and may create task records, reserve wallet balance, and settle a charge. Use the estimate endpoint first and obtain approval before any task request intended only for billing verification.
  • Never expose API keys, administrator JWTs, wallet identifiers, or credentials in reports.

Effective Billing Layers

Read every applicable layer before changing one:

  1. Base model: pricingRuleSetId and baseBillingConfig.
  2. Platform: pricingRuleSetId, defaultPricingMode, and defaultDiscountFactor.
  3. Platform model: pricingRuleSetId, billingConfigOverride, pricingMode, and discountFactor.
  4. User group: billingDiscountPolicy.discountFactor.

The runtime resolves an effective billing configuration, then applies discounts. An explicit platform-model rule and billingConfigOverride can replace or merge inherited values. Do not infer the final price from one ID; confirm it with an estimate.

The current discount behavior is:

candidate discount = positive platform-model discountFactor
                     else platform defaultDiscountFactor
effective discount = candidate discount × user-group billingDiscountPolicy.discountFactor

The platform and platform-model discount factors do not multiply together. The user-group factor, when positive, multiplies the selected candidate discount.

Pricing rule resource mappings include:

resourceType Effective use
text_input uncached input token price per 1,000 tokens
text_cached_input cached input token price per 1,000 tokens
text_output output token price per 1,000 tokens
text_total text prices from basePrice and optional formulaConfig
image image generation base price and dynamic weights
image_edit image-edit price; image pricing is a fallback when absent
video price per five-second unit plus dynamic weights
other types generic resource base price and weights, such as music or audio

Common dynamicWeight dimensions are qualityWeights, sizeWeights, resolutionWeights, audioWeights, referenceVideoWeights, and voiceSpecifiedWeights. Configure only dimensions used by the runtime and proven by the target product pricing.

Billing Configuration Workflow

1. Capture the current state

curl --fail-with-body -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  "$GATEWAY_BASE_URL/api/admin/pricing/rule-sets"
curl --fail-with-body -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  "$GATEWAY_BASE_URL/api/admin/catalog/base-models"
curl --fail-with-body -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  "$GATEWAY_BASE_URL/api/admin/platforms"
curl --fail-with-body -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  "$GATEWAY_BASE_URL/api/admin/models"
curl --fail-with-body -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  "$GATEWAY_BASE_URL/api/admin/user-groups"

Use the runtime subject credential to read its effective group policy and wallet snapshot:

curl --fail-with-body -H "Authorization: Bearer $RUNTIME_TOKEN" \
  "$GATEWAY_BASE_URL/api/workspace/user-groups"
curl --fail-with-body -H "Authorization: Bearer $RUNTIME_TOKEN" \
  "$GATEWAY_BASE_URL/api/workspace/wallet"

Save IDs and non-secret fields. Record the wallet balance and frozen balance only when a later task charge will be verified.

2. Reuse or create the pricing rule

Compare active rules by resource type, unit, base price, dynamicWeight, formulaConfig, calculator type, currency, and status. Reuse an existing rule when its effective amount can be adjusted with platform or platform-model discounts. Read model-pricing-and-policies.md for the rule-set request shape.

Create a new rule only when no existing rule can express the required unit, formula, dimensions, or undiscounted price. PATCH /api/admin/pricing/rule-sets/{ruleSetID} replaces all rules in the set, so preserve every required row.

3. Bind the minimum required layer

  • Put a reusable default rule on the base model with pricingRuleSetId.
  • Put account-wide pricing on the platform with pricingRuleSetId and defaultDiscountFactor.
  • Put a real per-model exception on the platform model with pricingRuleSetId, discountFactor, or billingConfigOverride.
  • Use a user-group billingDiscountPolicy.discountFactor only for a subject-wide discount. Read the complete user-group record before PATCH and preserve unrelated rate-limit, quota, recharge, metadata, and status fields.

Avoid copying the same prices into multiple layers. Read back every changed record before estimating.

Price Estimate Requests

POST /api/v1/pricing/estimate accepts a normal request-shaped JSON object plus kind. kind defaults to chat.completions. An API key must have the scope required by the selected kind.

Chat estimate

curl --fail-with-body \
  -H "Authorization: Bearer $RUNTIME_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "kind": "chat.completions",
    "model": "<model-alias>",
    "messages": [{"role": "user", "content": "Reply with OK"}],
    "max_tokens": 256
  }' \
  "$GATEWAY_BASE_URL/api/v1/pricing/estimate"

Text input tokens are estimated from the request. Output tokens use max_tokens; when it is absent or zero, the estimate currently uses 64 output tokens. Use the intended output limit for a meaningful upper-bound comparison.

Image estimate

curl --fail-with-body \
  -H "Authorization: Bearer $RUNTIME_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "kind": "images.generations",
    "model": "<model-alias>",
    "prompt": "A small orange cat",
    "n": 2,
    "size": "1024x1024",
    "quality": "high"
  }' \
  "$GATEWAY_BASE_URL/api/v1/pricing/estimate"

Video estimate

curl --fail-with-body \
  -H "Authorization: Bearer $RUNTIME_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "kind": "videos.generations",
    "model": "<model-alias>",
    "prompt": "A slow camera move over a lake",
    "duration": 12,
    "resolution": "1080p",
    "audio": true,
    "n": 1
  }' \
  "$GATEWAY_BASE_URL/api/v1/pricing/estimate"

The response has this shape:

{
  "items": [
    {
      "model": "example-model",
      "modelAlias": "example-model",
      "provider": "example",
      "platformId": "<platform-id>",
      "platformModelId": "<platform-model-id>",
      "resourceType": "video",
      "unit": "5s_video",
      "quantity": 2.4,
      "amount": 12.5,
      "currency": "resource",
      "discountFactor": 0.8,
      "simulated": true,
      "durationSeconds": 12,
      "durationUnitCount": 2.4
    }
  ],
  "resolver": "effective-pricing-v2",
  "totalAmount": 12.5,
  "currency": "resource"
}

Estimate Reconciliation

For every estimate, verify:

  1. platformId and platformModelId identify the intended first eligible candidate after permissions, capabilities, output-token limits, priority, and runtime availability are applied.
  2. resourceType, unit, quantity, and dimension details match the normalized request.
  3. discountFactor equals the selected platform/platform-model factor multiplied by the effective user-group factor.
  4. Each amount matches the configured base price, weights, quantity, and discount.
  5. totalAmount equals the rounded sum of items[].amount, and currency is expected.

Useful calculation checks:

text input  = input tokens / 1000 × input price × discount
text output = output tokens / 1000 × output price × discount
image       = count × base price × quality/size/resolution weights × discount
video       = count × (duration seconds / 5) × base price × applicable weights × discount
speech      = Unicode character count × audio price × discount

Cached input is normally known only after execution. When cached input exists but has no configured price, the runtime currently falls back to one tenth of the normal input price.

If the estimate selects an unexpected platform, do not edit prices to hide the routing problem. Inspect access rules, enabled state, model type, capabilities, priority, cooldown/load, and output-token limits first.

Task and Wallet Charge Verification

After the read-only estimate is accepted, obtain explicit approval before a simulation or real task if wallet mutation is possible.

  1. Record GET /api/workspace/wallet before the request.
  2. Submit the exact same kind, model, and billing-relevant parameters through the corresponding runtime API.
  3. Read GET /api/workspace/tasks/{taskID} and capture:
    • billings;
    • billingSummary.totalAmount and currency;
    • finalChargeAmount;
    • resolved model and candidate evidence from metrics.
  4. Query the final wallet debit:
curl --fail-with-body \
  -H "Authorization: Bearer $RUNTIME_TOKEN" \
  "$GATEWAY_BASE_URL/api/workspace/wallet/transactions?q=$TASK_ID&transactionType=task_billing&pageSize=20"
  1. Read the wallet again and compare:
task finalChargeAmount == billingSummary.totalAmount
task finalChargeAmount == task_billing transaction amount
wallet balance before - wallet balance after == task_billing transaction amount

The task may also create reserve and release transactions. Reservation changes frozen balance, not the spend balance; task_billing is the final debit to reconcile. Use the task ID as the reference and never sum reserve, release, and task_billing as three charges.

Expected Differences and Troubleshooting

An estimate and final charge may legitimately differ when:

  • actual text input/output or cached-input usage differs from the estimate;
  • the request omitted max_tokens, so the estimate used the 64-token default;
  • generated video duration or audio presence overrides the requested/preprocessed value;
  • preprocessing normalizes duration, resolution, count, or model-specific fields;
  • the first candidate fails and execution settles on another platform with different pricing;
  • the estimate and task used different users, API keys, scopes, groups, or access rules;
  • pricing or discount configuration changed between estimate and execution.

When the difference is unexplained, compare the estimate line, task billings, task preprocessing log, candidate metrics, effective user group, wallet transaction metadata, and configuration timestamps. Do not change wallet balances to make a mismatch disappear.