# 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:

```text
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

```bash
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:

```bash
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

```bash
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

```bash
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

```bash
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:

```json
{
  "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
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:

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $RUNTIME_TOKEN" \
  "$GATEWAY_BASE_URL/api/workspace/wallet/transactions?q=$TASK_ID&transactionType=task_billing&pageSize=20"
```

5. Read the wallet again and compare:

```text
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.
