feat: 增加计费预估核对运维流程

This commit is contained in:
wangbo 2026-07-17 13:57:21 +08:00
parent bdcf9af5d6
commit 0d18df54f1
11 changed files with 289 additions and 17 deletions

View File

@ -10653,7 +10653,7 @@
},
"fileName": {
"type": "string",
"example": "ai-gateway-ops-management-v1.0.1.zip"
"example": "ai-gateway-ops-management-v1.0.2.zip"
},
"modules": {
"type": "array",
@ -10670,7 +10670,7 @@
},
"version": {
"type": "string",
"example": "1.0.1"
"example": "1.0.2"
}
}
},

View File

@ -791,7 +791,7 @@ definitions:
example: /api/v1/public/skills/ai-gateway-ops-management/download
type: string
fileName:
example: ai-gateway-ops-management-v1.0.1.zip
example: ai-gateway-ops-management-v1.0.2.zip
type: string
modules:
example:
@ -803,7 +803,7 @@ definitions:
example: ai-gateway-ops-management
type: string
version:
example: 1.0.1
example: 1.0.2
type: string
type: object
httpapi.TaskAcceptedResponse:

View File

@ -26,7 +26,7 @@ func TestGetOpsManagementSkillMetadata(t *testing.T) {
if err := json.Unmarshal(response.Body.Bytes(), &metadata); err != nil {
t.Fatalf("decode metadata: %v", err)
}
if metadata.Name != "ai-gateway-ops-management" || metadata.Version != "1.0.1" {
if metadata.Name != "ai-gateway-ops-management" || metadata.Version != "1.0.2" {
t.Fatalf("unexpected metadata: %+v", metadata)
}
if len(metadata.Modules) != 1 || metadata.Modules[0] != "model-runtime" {
@ -50,7 +50,7 @@ func TestDownloadOpsManagementSkill(t *testing.T) {
if response.Header().Get("Content-Type") != "application/zip" {
t.Fatalf("unexpected content type: %q", response.Header().Get("Content-Type"))
}
if disposition := response.Header().Get("Content-Disposition"); !strings.Contains(disposition, "ai-gateway-ops-management-v1.0.1.zip") {
if disposition := response.Header().Get("Content-Disposition"); !strings.Contains(disposition, "ai-gateway-ops-management-v1.0.2.zip") {
t.Fatalf("unexpected content disposition: %q", disposition)
}
raw := response.Body.Bytes()

View File

@ -18,10 +18,10 @@ type ReadyResponse struct {
type SkillBundleMetadataResponse struct {
Name string `json:"name" example:"ai-gateway-ops-management"`
Version string `json:"version" example:"1.0.1"`
Version string `json:"version" example:"1.0.2"`
DisplayName string `json:"displayName" example:"AI Gateway 运维管理"`
Modules []string `json:"modules" example:"model-runtime"`
FileName string `json:"fileName" example:"ai-gateway-ops-management-v1.0.1.zip"`
FileName string `json:"fileName" example:"ai-gateway-ops-management-v1.0.2.zip"`
DownloadPath string `json:"downloadPath" example:"/api/v1/public/skills/ai-gateway-ops-management/download"`
APIDocsJSONPath string `json:"apiDocsJsonPath" example:"/api-docs-json"`
APIDocsYAMLPath string `json:"apiDocsYamlPath" example:"/api-docs-yaml"`

View File

@ -14,13 +14,13 @@ func TestLoadMetadata(t *testing.T) {
if err != nil {
t.Fatalf("load metadata: %v", err)
}
if metadata.Name != Name || metadata.Version != "1.0.1" {
if metadata.Name != Name || metadata.Version != "1.0.2" {
t.Fatalf("unexpected metadata: %+v", metadata)
}
if !slices.Equal(metadata.Modules, []string{"model-runtime"}) {
t.Fatalf("unexpected modules: %+v", metadata.Modules)
}
if FileName(metadata) != "ai-gateway-ops-management-v1.0.1.zip" {
if FileName(metadata) != "ai-gateway-ops-management-v1.0.2.zip" {
t.Fatalf("unexpected file name: %s", FileName(metadata))
}
}
@ -48,6 +48,7 @@ func TestBuildArchiveContainsOperationsSkill(t *testing.T) {
"references/api-discovery-and-safety.md",
"references/model-providers-and-base-models.md",
"references/model-pricing-and-policies.md",
"references/model-billing-configuration-and-estimation.md",
"references/model-platforms-and-bindings.md",
"references/model-universal-platforms.md",
"references/model-acceptance-runbook.md",
@ -69,6 +70,20 @@ func TestBuildArchiveContainsOperationsSkill(t *testing.T) {
if !strings.Contains(string(skillContent), "name: "+Name) {
t.Fatalf("SKILL.md frontmatter has unexpected name")
}
billingFile, err := files["references/model-billing-configuration-and-estimation.md"].Open()
if err != nil {
t.Fatalf("open billing reference: %v", err)
}
defer billingFile.Close()
billingContent, err := io.ReadAll(billingFile)
if err != nil {
t.Fatalf("read billing reference: %v", err)
}
for _, required := range []string{"/api/v1/pricing/estimate", "finalChargeAmount", "transactionType=task_billing"} {
if !strings.Contains(string(billingContent), required) {
t.Fatalf("billing reference missing %q", required)
}
}
}
func archiveFileNames(files []*zip.File) []string {

View File

@ -1,6 +1,6 @@
---
name: ai-gateway-ops-management
description: Operate and verify EasyAI AI Gateway administration capabilities. Use when Codex or an Agent needs to inspect, create, update, disable, restore, or troubleshoot AI Gateway providers, base models, pricing, runtime policies, runner policies, integration platforms, platform-model bindings, or universal custom-script platforms; also use this skill as the extensible entry point for future AI Gateway operations modules.
description: Operate and verify EasyAI AI Gateway administration capabilities. Use when Codex or an Agent needs to inspect, create, update, disable, restore, or troubleshoot AI Gateway providers, base models, pricing and billing configuration, price estimates and wallet deduction reconciliation, runtime policies, runner policies, integration platforms, platform-model bindings, or universal custom-script platforms; also use this skill as the extensible entry point for future AI Gateway operations modules.
---
# AI Gateway Operations Management
@ -28,6 +28,7 @@ Use these references for the current v1 module:
- `references/model-providers-and-base-models.md`: provider catalog and base-model lifecycle.
- `references/model-pricing-and-policies.md`: pricing rule sets, runtime policy sets, runner policy, priority, and recovery.
- `references/model-billing-configuration-and-estimation.md`: effective billing configuration, price-estimate calls, simulation/real charge comparison, and wallet reconciliation.
- `references/model-platforms-and-bindings.md`: integration platforms, credentials, platform-model upsert, replacement, and deletion.
- `references/model-universal-platforms.md`: `universal` custom platform triage, configuration, script contracts, and examples.
- `references/model-acceptance-runbook.md`: read-back, catalog, simulation, runtime, billing, and rollback verification.
@ -42,8 +43,9 @@ Use these references for the current v1 module:
6. Reuse the base model across platforms. Put a platform-specific upstream invocation name in the platform-model `providerModelName`; do not duplicate the base model just because providers use different names.
7. Apply the minimum changes in dependency order: provider, base model, pricing/policy, platform, platform-model binding.
8. Read every changed resource back and verify the effective model catalog.
9. Run a simulation or approved real request, inspect task and preprocessing evidence, and confirm billing and recovery behavior.
10. Report reused and created resource IDs, effective pricing evidence, protocol-fit evidence, verification results, unresolved risks, and whether the Swagger fallback was used, without exposing secrets.
9. For billing work, read `references/model-billing-configuration-and-estimation.md`, call `/api/v1/pricing/estimate` with the same user identity and request parameters intended for execution, and reconcile the returned candidate, line items, discounts, quantities, and total.
10. Run a simulation or approved real request only after the estimate is accepted. Inspect task billing and wallet transactions; do not assume simulation is free or side-effect-free.
11. Report reused and created resource IDs, effective pricing evidence, protocol-fit evidence, verification results, unresolved risks, and whether the Swagger fallback was used, without exposing secrets.
## Extending This Skill

View File

@ -8,6 +8,7 @@
- Confirm whether real upstream calls are allowed; otherwise use simulation.
- Confirm upstream endpoint, auth, model name, capabilities, limits, pricing, and sync/async behavior from documentation.
- Record which existing pricing rule and runtime policy were reused, or the exact semantic mismatch that required a new one.
- For billing changes, save the current effective rule bindings, discounts, user-group policy, wallet balance, and one representative estimate response as described in `model-billing-configuration-and-estimation.md`.
- Record the protocol compatibility decision and why the selected existing `specType` is sufficient, or the concrete gap that requires `universal`.
- When one base model has different upstream names across platforms, confirm each binding resolves the expected `providerModelName`.
@ -39,6 +40,8 @@ Confirm model alias, model types, provider source, effective capabilities, prici
For pricing, verify the effective rule source and discount source rather than checking IDs only. Confirm whether the platform-model discount overrides the platform default, and compare the estimated or simulated amount with the intended price.
Use `/api/v1/pricing/estimate` for the first read-only calculation. A simulation request is a task execution path and may reserve and settle wallet billing; do not treat it as a read-only replacement for the estimate endpoint.
## Execution Verification
Start with simulation:
@ -67,7 +70,7 @@ For media or universal scripts, inspect:
- `/api/admin/runtime/model-rate-limits`
- `/api/admin/runtime/rate-limit-windows`
With explicit approval, run one real minimal request and verify upstream request ID, normalized output, task completion, billing, and failure handling.
With explicit approval, run one real minimal request and verify upstream request ID, normalized output, task completion, `billings`, `billingSummary.totalAmount`, `finalChargeAmount`, and the wallet `task_billing` transaction. Explain any expected estimate difference caused by actual token usage, cached input, generated media duration/audio, preprocessing, or failover to another platform.
## Rollback

View File

@ -0,0 +1,249 @@
# 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": 3,
"amount": 12.5,
"currency": "resource",
"discountFactor": 0.8,
"simulated": true,
"durationSeconds": 12,
"durationUnitCount": 3
}
],
"resolver": "effective-pricing-v1",
"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 × ceil(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.

View File

@ -3,6 +3,7 @@
## Contents
- Pricing Rule Sets
- Billing Configuration and Estimate Reconciliation
- Runtime Policy Sets
- Runner Policy and Runtime Recovery
@ -37,6 +38,8 @@ Evaluate the price that the runtime will actually use:
Do not clone an otherwise identical pricing rule merely to represent a provider or account discount. Keep the reusable base price in the rule and express the instance-specific adjustment with `defaultDiscountFactor` or `discountFactor`.
For the complete binding, estimate, task-charge, and wallet-reconciliation workflow, read `model-billing-configuration-and-estimation.md` before changing billing settings.
Create an example text pricing rule set:
```bash

View File

@ -1,6 +1,6 @@
{
"name": "ai-gateway-ops-management",
"version": "1.0.1",
"version": "1.0.2",
"modules": [
"model-runtime"
]

View File

@ -75,10 +75,10 @@ describe('Public Agent resources', () => {
it('loads operations skill metadata without authorization', async () => {
const metadata = {
name: 'ai-gateway-ops-management',
version: '1.0.1',
version: '1.0.2',
displayName: 'AI Gateway 运维管理',
modules: ['model-runtime'],
fileName: 'ai-gateway-ops-management-v1.0.1.zip',
fileName: 'ai-gateway-ops-management-v1.0.2.zip',
downloadPath: '/api/v1/public/skills/ai-gateway-ops-management/download',
apiDocsJsonPath: '/api-docs-json',
apiDocsYamlPath: '/api-docs-yaml',