fix: migration conflicts

This commit is contained in:
johnyeo
2026-06-11 09:11:51 +01:00
661 changed files with 80193 additions and 8526 deletions

View File

@@ -26,7 +26,7 @@ env:
# staging repo (autumn-staging) -> us-east-1
# Branches allowed to deploy to staging via workflow_dispatch with tag=deploy-staging.
# Add short-lived PR branches here when you need staging without merging to dev.
STAGING_DEPLOY_BRANCH_ALLOWLIST: fix-health-check-redis-disabled-detection feat/track-rate-limit-redis feat/events-hourly-rollup fix/analytics-tz-bucket-offset
STAGING_DEPLOY_BRANCH_ALLOWLIST: ""
jobs:
checks:

1
.gitignore vendored
View File

@@ -22,6 +22,7 @@ supabase.sh
tests/
!server/tests
!packages/mcp/tests
!packages/ai-sdk/tests
!apps/leaf/tests
!vite/tests
.secrets

View File

@@ -0,0 +1,78 @@
---
title: "Track Token Usage"
openapi: "openapi POST /v1/balances.track_tokens"
---
import { DynamicParamField } from "/snippets/dynamic-param-field.jsx";
import { DynamicResponseField } from "/snippets/dynamic-response-field.jsx";
import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
<Note>
Track AI token usage against a customer's AI credit system balance. Converts token counts to a dollar cost using [Models.dev](https://models.dev) pricing and your configured markup, then deducts from the customer's credit balance.
</Note>
The `model_id` must use `provider/model` format, matching the provider and model keys from [Models.dev](https://models.dev). For providers with nested model paths (like OpenRouter), include the full path: `openrouter/anthropic/claude-opus-4.6`. The first path segment is the provider key used for `providerMarkups` lookup.
### Common Use Cases
<CodeGroup>
```typescript Anthropic
await autumn.balances.trackTokens({
customerId: "cus_123",
modelId: "anthropic/claude-opus-4-6",
inputTokens: 1000,
outputTokens: 500
});
```
```typescript With cache + reasoning
await autumn.balances.trackTokens({
customerId: "cus_123",
modelId: "anthropic/claude-opus-4-6",
inputTokens: 800, // excludes the cached tokens below
outputTokens: 350, // excludes the reasoning tokens below
cacheReadTokens: 1000,
cacheWriteTokens: 200,
reasoningTokens: 150
});
```
```typescript OpenRouter (nested path)
await autumn.balances.trackTokens({
customerId: "cus_123",
modelId: "openrouter/anthropic/claude-opus-4-6",
inputTokens: 2000,
outputTokens: 1000
});
```
```typescript With explicit feature
await autumn.balances.trackTokens({
customerId: "cus_123",
featureId: "ai_credits",
modelId: "anthropic/claude-haiku-4-5",
inputTokens: 2000,
outputTokens: 1000
});
```
</CodeGroup>
### Token Pools
Each token parameter is an exclusive pool — no token should be counted in more than one. `input_tokens` is non-cached text input only (cached tokens go in `cache_read_tokens` / `cache_write_tokens`), and `output_tokens` is text output only (reasoning tokens go in `reasoning_tokens`, audio in `audio_input_tokens` / `audio_output_tokens`). Each pool is billed at the model's published rate for that pool, falling back to the text input/output rate when the model has none.
<Warning>
If you pass a provider's raw totals (e.g. OpenAI's `prompt_tokens` and `completion_tokens`), subtract the cache and reasoning counts first — otherwise those tokens are billed twice. The [`@useautumn/ai-sdk` wrapper](/documentation/external-providers/ai-sdk) does this normalization for you.
</Warning>
### Markup Resolution
Markups are optional — the credit system's default markup applies unless overridden per provider or per model. With no markups set, the Models.dev base cost is charged as-is. A markup of `-100` makes the model free — the usage event is still recorded, but nothing is deducted. See [AI Credit Systems](/documentation/modelling-pricing/credit-systems#ai-credit-systems) for configuration.
The recorded event's `properties` include the full pricing breakdown: `cost`, `base_cost`, `markup`, `markup_source` (`model`, `provider`, or `default`), `tier_applied` (whether large-context tier pricing applied), and the per-pool `rates` used.
<Tip>
`feature_id` is auto-detected when the customer has exactly one AI credit system. The request fails if the customer has none, or has more than one and `feature_id` is omitted.
</Tip>

View File

@@ -0,0 +1,206 @@
---
title: "Track Token Usage"
openapi: "openapi POST /v1/balances.track_tokens"
---
import { DynamicParamField } from "/snippets/dynamic-param-field.jsx";
import { DynamicResponseField } from "/snippets/dynamic-response-field.jsx";
import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
<Note>
Track AI token usage against a customer's AI credit system balance. Converts token counts to a dollar cost using [Models.dev](https://models.dev) pricing and your configured markup, then deducts from the customer's credit balance.
</Note>
The `model_id` must use `provider/model` format, matching the provider and model keys from [Models.dev](https://models.dev). For providers with nested model paths (like OpenRouter), include the full path: `openrouter/anthropic/claude-opus-4.6`. The first path segment is the provider key used for provider-level markup lookup.
### Common Use Cases
<CodeGroup>
```typescript Anthropic
await autumn.balances.trackTokens({
customerId: "cus_123",
modelId: "anthropic/claude-opus-4-6",
inputTokens: 1000,
outputTokens: 500
});
```
```typescript With cache + reasoning
await autumn.balances.trackTokens({
customerId: "cus_123",
modelId: "anthropic/claude-opus-4-6",
inputTokens: 800, // excludes the cached tokens below
outputTokens: 350, // excludes the reasoning tokens below
cacheReadTokens: 1000,
cacheWriteTokens: 200,
reasoningTokens: 150
});
```
```typescript OpenRouter (nested path)
await autumn.balances.trackTokens({
customerId: "cus_123",
modelId: "openrouter/anthropic/claude-opus-4.6",
inputTokens: 2000,
outputTokens: 1000
});
```
```typescript With explicit feature
await autumn.balances.trackTokens({
customerId: "cus_123",
featureId: "ai_credits",
modelId: "anthropic/claude-haiku-4-5",
inputTokens: 2000,
outputTokens: 1000
});
```
</CodeGroup>
### Token Pools
Each token parameter is an exclusive pool — no token should be counted in more than one. Each pool is billed at the model's published rate for that pool, falling back to the text input/output rate when the model has none.
<Warning>
If you pass a provider's raw totals (e.g. OpenAI's `prompt_tokens` and `completion_tokens`), subtract the cache and reasoning counts first — otherwise those tokens are billed twice. The [`@useautumn/ai-sdk` wrapper](/documentation/external-providers/ai-sdk) does this normalization for you.
</Warning>
### Markup Resolution
Markups are optional — the credit system's default markup applies unless overridden per provider or per model. With no markups set, the Models.dev base cost is charged as-is. A markup of `-100` makes the model free — the usage event is still recorded, but nothing is deducted. See [AI Credit Systems](/documentation/modelling-pricing/credit-systems#ai-credit-systems) for configuration.
<Tip>
`feature_id` is auto-detected when the customer has exactly one AI credit system. The request fails if the customer has none, or has more than one and `feature_id` is omitted.
</Tip>
### Body Parameters
<DynamicParamField body="customer_id" type="string" required>
The ID of the customer.
</DynamicParamField>
<DynamicParamField body="model_id" type="string" required>
The AI model in `provider/model` format, matching keys from [Models.dev](https://models.dev) (e.g., `anthropic/claude-opus-4-6`, `openai/gpt-4o`, `openrouter/anthropic/claude-opus-4.6`).
</DynamicParamField>
<DynamicParamField body="input_tokens" type="number" required>
Number of non-cached text input tokens consumed. Exclusive of the cache and audio token pools.
</DynamicParamField>
<DynamicParamField body="output_tokens" type="number" required>
Number of text output tokens consumed. Exclusive of the reasoning and audio output pools.
</DynamicParamField>
<DynamicParamField body="cache_read_tokens" type="number">
Number of cached input tokens read, billed at the model's cache read rate.
</DynamicParamField>
<DynamicParamField body="cache_write_tokens" type="number">
Number of input tokens written to the cache, billed at the model's cache write rate.
</DynamicParamField>
<DynamicParamField body="reasoning_tokens" type="number">
Number of reasoning tokens generated, billed at the model's reasoning rate (falls back to the output rate).
</DynamicParamField>
<DynamicParamField body="audio_input_tokens" type="number">
Number of audio input tokens consumed, billed at the model's audio input rate (falls back to the input rate).
</DynamicParamField>
<DynamicParamField body="audio_output_tokens" type="number">
Number of audio output tokens generated, billed at the model's audio output rate (falls back to the output rate).
</DynamicParamField>
<DynamicParamField body="feature_id" type="string">
The ID of the AI credit system feature. If omitted, automatically detects the customer's AI credit system feature. Required when the customer has more than one.
</DynamicParamField>
<DynamicParamField body="entity_id" type="string">
The ID of the entity for entity-scoped balances.
</DynamicParamField>
<DynamicParamField body="properties" type="object">
Additional properties to attach to this usage event. The token counts and a pricing breakdown (`cost`, `base_cost`, `markup`, `markup_source`, `tier_applied`, `rates`) are automatically included.
</DynamicParamField>
### Response
<DynamicResponseField name="customer_id" type="string">
The ID of the customer whose token usage was tracked.
</DynamicResponseField>
<DynamicResponseField name="value" type="number">
The dollar cost that was deducted from the customer's AI credit balance.
</DynamicResponseField>
<DynamicResponseField name="balance" type="object | null">
The updated balance for the AI credit system feature.
<Expandable title="properties">
<DynamicResponseField name="feature_id" type="string">
The feature ID this balance is for.
</DynamicResponseField>
<DynamicResponseField name="granted" type="number">
Total balance granted (included + prepaid).
</DynamicResponseField>
<DynamicResponseField name="remaining" type="number">
Remaining balance available for use.
</DynamicResponseField>
<DynamicResponseField name="usage" type="number">
Total usage consumed in the current period.
</DynamicResponseField>
<DynamicResponseField name="unlimited" type="boolean">
Whether this feature has unlimited usage.
</DynamicResponseField>
<DynamicResponseField name="overage_allowed" type="boolean">
Whether usage beyond the granted balance is allowed.
</DynamicResponseField>
<DynamicResponseField name="next_reset_at" type="number | null">
Timestamp when the balance will reset, or null for no reset.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<ResponseExample>
```json 200
{
"customer_id": "cus_123",
"value": 0.06,
"balance": {
"feature_id": "ai_credits",
"granted": 10.00,
"remaining": 9.94,
"usage": 0.06,
"unlimited": false,
"overage_allowed": false,
"next_reset_at": 1773851121437,
"breakdown": [
{
"id": "cus_ent_abc123",
"plan_id": "pro_plan",
"included_grant": 10.00,
"prepaid_grant": 0,
"remaining": 9.94,
"usage": 0.06,
"unlimited": false,
"reset": {
"interval": "month",
"resets_at": 1773851121437
},
"price": null,
"expires_at": null
}
]
}
}
```
</ResponseExample>

View File

@@ -132,7 +132,7 @@ This is useful for attaching custom metadata to the Stripe subscription created
</DynamicParamField>
<DynamicParamField body="items" type="object[]">
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items.
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items.
<Expandable title="properties">
<DynamicParamField body="feature_id" type="string" required>
The ID of the feature to configure.
@@ -197,8 +197,8 @@ This is useful for attaching custom metadata to the Stripe subscription created
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -309,8 +309,8 @@ This is useful for attaching custom metadata to the Stripe subscription created
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -367,7 +367,11 @@ This is useful for attaching custom metadata to the Stripe subscription created
</DynamicParamField>
<DynamicParamField body="interval" type="'one_off' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
Match items with this interval.
Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated.
</DynamicParamField>
<DynamicParamField body="interval_count" type="integer">
Match items with this interval_count. Disambiguates between items that share an interval but differ in count.
</DynamicParamField>
</Expandable>

View File

@@ -99,7 +99,7 @@ const response = await autumn.billing.update({
</DynamicParamField>
<DynamicParamField body="items" type="object[]">
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items.
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items.
<Expandable title="properties">
<DynamicParamField body="feature_id" type="string" required>
The ID of the feature to configure.
@@ -164,8 +164,8 @@ const response = await autumn.billing.update({
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -276,8 +276,8 @@ const response = await autumn.billing.update({
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -334,7 +334,11 @@ const response = await autumn.billing.update({
</DynamicParamField>
<DynamicParamField body="interval" type="'one_off' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
Match items with this interval.
Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated.
</DynamicParamField>
<DynamicParamField body="interval_count" type="integer">
Match items with this interval_count. Disambiguates between items that share an interval but differ in count.
</DynamicParamField>
</Expandable>

View File

@@ -118,7 +118,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="customize" type="object">
Customize the plan to schedule. Can override the price, items, or both.
Customize the plan to schedule. Can override price, replace items, or patch items with add_items, remove_items, and update_items.
<Expandable title="properties">
<DynamicParamField body="price" type="object | null">
Base price configuration for a plan.
@@ -139,7 +139,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="items" type="object[]">
Override the items in the plan.
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items.
<Expandable title="properties">
<DynamicParamField body="feature_id" type="string" required>
The ID of the feature to configure.
@@ -204,8 +204,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -250,6 +250,140 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicParamField>
<DynamicParamField body="add_items" type="object[]">
Items to add to the plan.
<Expandable title="properties">
<DynamicParamField body="feature_id" type="string" required>
The ID of the feature to configure.
</DynamicParamField>
<DynamicParamField body="included" type="number">
Number of free units included. Balance resets to this each interval for consumable features.
</DynamicParamField>
<DynamicParamField body="unlimited" type="boolean">
If true, customer has unlimited access to this feature.
</DynamicParamField>
<DynamicParamField body="reset" type="object">
Reset configuration for consumable features. Omit for non-consumable features like seats.
<Expandable title="properties">
<DynamicParamField body="interval" type="'one_off' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'" required>
Interval at which balance resets (e.g. 'month', 'year'). For consumable features only.
</DynamicParamField>
<DynamicParamField body="interval_count" type="number">
Number of intervals between resets. Defaults to 1.
</DynamicParamField>
</Expandable>
</DynamicParamField>
<DynamicParamField body="price" type="object">
Pricing for usage beyond included units. Omit for free features.
<Expandable title="properties">
<DynamicParamField body="amount" type="number">
Price per billing_units after included usage. Either 'amount' or 'tiers' is required.
</DynamicParamField>
<DynamicParamField body="tiers" type="object[]">
Tiered pricing. Either 'amount' or 'tiers' is required.
<Expandable title="properties">
<DynamicParamField body="to" type="any" />
<DynamicParamField body="amount" type="any" />
<DynamicParamField body="flat_amount" type="any" />
</Expandable>
</DynamicParamField>
<DynamicParamField body="tier_behavior" type="'graduated' | 'volume'" />
<DynamicParamField body="interval" type="'one_off' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'" required>
Billing interval. For consumable features, should match reset.interval.
</DynamicParamField>
<DynamicParamField body="interval_count" type="number">
Number of intervals per billing cycle. Defaults to 1.
</DynamicParamField>
<DynamicParamField body="billing_units" type="number">
Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200).
</DynamicParamField>
<DynamicParamField body="billing_method" type="'prepaid' | 'usage_based'" required>
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
</DynamicParamField>
<DynamicParamField body="proration" type="object">
Proration settings for prepaid features. Controls mid-cycle quantity change billing.
<Expandable title="properties">
<DynamicParamField body="on_increase" type="'bill_immediately' | 'prorate_immediately' | 'prorate_next_cycle' | 'bill_next_cycle'" required>
Billing behavior when quantity increases mid-cycle.
</DynamicParamField>
<DynamicParamField body="on_decrease" type="'prorate' | 'prorate_immediately' | 'prorate_next_cycle' | 'none' | 'no_prorations'" required>
Credit behavior when quantity decreases mid-cycle.
</DynamicParamField>
</Expandable>
</DynamicParamField>
<DynamicParamField body="rollover" type="object">
Rollover config for unused units. If set, unused included units carry over.
<Expandable title="properties">
<DynamicParamField body="max" type="number">
Max rollover units. Omit for unlimited rollover.
</DynamicParamField>
<DynamicParamField body="max_percentage" type="number">
Maximum rollover as a percentage (0-100) of included + prepaid grant. Mutually exclusive with max.
</DynamicParamField>
<DynamicParamField body="expiry_duration_type" type="'month' | 'forever'" required>
When rolled over units expire.
</DynamicParamField>
<DynamicParamField body="expiry_duration_length" type="number">
Number of periods before expiry.
</DynamicParamField>
</Expandable>
</DynamicParamField>
</Expandable>
</DynamicParamField>
<DynamicParamField body="remove_items" type="object[]">
Filters selecting items to remove from the plan.
<Expandable title="properties">
<DynamicParamField body="feature_id" type="string">
Match items linked to this feature.
</DynamicParamField>
<DynamicParamField body="billing_method" type="'prepaid' | 'usage_based'">
Match items with this billing method (prepaid or usage_based).
</DynamicParamField>
<DynamicParamField body="interval" type="'one_off' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated.
</DynamicParamField>
<DynamicParamField body="interval_count" type="integer">
Match items with this interval_count. Disambiguates between items that share an interval but differ in count.
</DynamicParamField>
</Expandable>
</DynamicParamField>
</Expandable>
</DynamicParamField>

View File

@@ -111,8 +111,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>

View File

@@ -65,7 +65,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="items" type="object[]">
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items.
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items.
<Expandable title="properties">
<DynamicParamField body="feature_id" type="string" required>
The ID of the feature to configure.
@@ -130,8 +130,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -242,8 +242,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -300,7 +300,11 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="interval" type="'one_off' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
Match items with this interval.
Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated.
</DynamicParamField>
<DynamicParamField body="interval_count" type="integer">
Match items with this interval_count. Disambiguates between items that share an interval but differ in count.
</DynamicParamField>
</Expandable>
@@ -757,7 +761,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -825,8 +829,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -1086,7 +1098,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -1154,8 +1166,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />

View File

@@ -111,8 +111,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -788,7 +788,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -856,8 +856,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -1117,7 +1125,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -1185,8 +1193,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />

View File

@@ -65,7 +65,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="items" type="object[]">
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items.
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items.
<Expandable title="properties">
<DynamicParamField body="feature_id" type="string" required>
The ID of the feature to configure.
@@ -130,8 +130,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -242,8 +242,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -300,7 +300,11 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="interval" type="'one_off' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
Match items with this interval.
Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated.
</DynamicParamField>
<DynamicParamField body="interval_count" type="integer">
Match items with this interval_count. Disambiguates between items that share an interval but differ in count.
</DynamicParamField>
</Expandable>
@@ -690,7 +694,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -758,8 +762,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -1019,7 +1031,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -1087,8 +1099,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />

View File

@@ -65,7 +65,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="items" type="object[]">
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items.
Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items.
<Expandable title="properties">
<DynamicParamField body="feature_id" type="string" required>
The ID of the feature to configure.
@@ -130,8 +130,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -242,8 +242,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -300,7 +300,11 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="interval" type="'one_off' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
Match items with this interval.
Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated.
</DynamicParamField>
<DynamicParamField body="interval_count" type="integer">
Match items with this interval_count. Disambiguates between items that share an interval but differ in count.
</DynamicParamField>
</Expandable>

View File

@@ -124,8 +124,8 @@ const { allowed } = await autumn.check({
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -150,6 +150,30 @@ const { allowed } = await autumn.check({
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -255,8 +279,16 @@ const { allowed } = await autumn.check({
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -320,8 +352,8 @@ const { allowed } = await autumn.check({
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -346,6 +378,30 @@ const { allowed } = await autumn.check({
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -451,8 +507,16 @@ const { allowed } = await autumn.check({
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -528,8 +592,8 @@ const { allowed } = await autumn.check({
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -554,6 +618,30 @@ const { allowed } = await autumn.check({
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -135,8 +135,8 @@ await autumn.track({
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -161,6 +161,30 @@ await autumn.track({
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -266,8 +290,16 @@ await autumn.track({
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -331,8 +363,8 @@ await autumn.track({
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -357,6 +389,30 @@ await autumn.track({
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -462,8 +518,16 @@ await autumn.track({
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">

View File

@@ -0,0 +1,622 @@
---
title: "Track Tokens"
openapi: "openapi POST /v1/balances.track_tokens"
---
import { DynamicParamField } from "/snippets/dynamic-param-field.jsx";
import { DynamicResponseField } from "/snippets/dynamic-response-field.jsx";
import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
### Body Parameters
<DynamicParamField body="customer_id" type="string" required>
The ID of the customer.
</DynamicParamField>
<DynamicParamField body="entity_id" type="string">
The ID of the entity for entity-scoped balances.
</DynamicParamField>
<DynamicParamField body="feature_id" type="string">
The ID of the AI credit system feature. Auto-detected from the customer's entitlements if omitted — only required when a customer has multiple AI credit systems.
</DynamicParamField>
<DynamicParamField body="model_id" type="string" required>
The AI model as '<provider>/<model>' (e.g. 'anthropic/claude-opus-4-8', 'openrouter/openai/gpt-4o'). The provider is the first path segment and must match a provider + model key in models.dev.
</DynamicParamField>
<DynamicParamField body="input_tokens" type="integer" required>
Number of non-cached text input tokens consumed. Exclusive of cache and audio token pools.
</DynamicParamField>
<DynamicParamField body="output_tokens" type="integer" required>
Number of text output tokens consumed. Exclusive of the reasoning and audio output pools.
</DynamicParamField>
<DynamicParamField body="cache_read_tokens" type="integer">
Number of cached input tokens read.
</DynamicParamField>
<DynamicParamField body="cache_write_tokens" type="integer">
Number of input tokens written to the cache.
</DynamicParamField>
<DynamicParamField body="audio_input_tokens" type="integer">
Number of audio input tokens consumed.
</DynamicParamField>
<DynamicParamField body="audio_output_tokens" type="integer">
Number of audio output tokens generated.
</DynamicParamField>
<DynamicParamField body="reasoning_tokens" type="integer">
Number of reasoning tokens generated.
</DynamicParamField>
<DynamicParamField body="properties" type="object">
Additional properties to attach to this usage event.
</DynamicParamField>
### Response
<DynamicResponseField name="customer_id" type="string">
The ID of the customer whose usage was tracked.
</DynamicResponseField>
<DynamicResponseField name="entity_id" type="string">
The ID of the entity, if entity-scoped tracking was performed.
</DynamicResponseField>
<DynamicResponseField name="event_name" type="string">
The event name that was tracked, if event_name was used instead of feature_id.
</DynamicResponseField>
<DynamicResponseField name="value" type="number">
The amount of usage that was recorded.
</DynamicResponseField>
<DynamicResponseField name="balance" type="object | null">
The updated balance for the tracked feature. Null if tracking by event_name that affects multiple features.
<Expandable title="properties">
<DynamicResponseField name="feature_id" type="string">
The feature ID this balance is for.
</DynamicResponseField>
<DynamicResponseField name="feature" type="object">
The full feature object if expanded.
<Expandable title="properties">
<DynamicResponseField name="id" type="string">
The unique identifier for this feature, used in /check and /track calls.
</DynamicResponseField>
<DynamicResponseField name="name" type="string">
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage).
</DynamicResponseField>
<DynamicResponseField name="event_names" type="string[]">
Event names that trigger this feature's balance. Allows multiple features to respond to a single event.
</DynamicResponseField>
<DynamicResponseField name="credit_schema" type="object[]">
For credit_system features: maps metered features to their credit costs.
<Expandable title="properties">
<DynamicResponseField name="metered_feature_id" type="string">
ID of the metered feature that draws from this credit system.
</DynamicResponseField>
<DynamicResponseField name="credit_cost" type="number">
Credits consumed per unit of the metered feature.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
<DynamicResponseField name="singular" type="string | null">
Singular form for UI display (e.g., 'API call', 'seat').
</DynamicResponseField>
<DynamicResponseField name="plural" type="string | null">
Plural form for UI display (e.g., 'API calls', 'seats').
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="archived" type="boolean">
Whether the feature is archived and hidden from the dashboard.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="granted" type="number">
Total balance granted (included + prepaid).
</DynamicResponseField>
<DynamicResponseField name="remaining" type="number">
Remaining balance available for use.
</DynamicResponseField>
<DynamicResponseField name="usage" type="number">
Total usage consumed in the current period.
</DynamicResponseField>
<DynamicResponseField name="unlimited" type="boolean">
Whether this feature has unlimited usage.
</DynamicResponseField>
<DynamicResponseField name="overage_allowed" type="boolean">
Whether usage beyond the granted balance is allowed (with overage charges).
</DynamicResponseField>
<DynamicResponseField name="max_purchase" type="number | null">
Maximum quantity that can be purchased as a top-up, or null for unlimited.
</DynamicResponseField>
<DynamicResponseField name="next_reset_at" type="number | null">
Timestamp when the balance will reset, or null for no reset.
</DynamicResponseField>
<DynamicResponseField name="breakdown" type="object[]">
Detailed breakdown of balance sources when stacking multiple plans or grants.
<Expandable title="properties">
<DynamicResponseField name="id" type="string">
The unique identifier for this balance breakdown.
</DynamicResponseField>
<DynamicResponseField name="plan_id" type="string | null">
The plan ID this balance originates from, or null for standalone balances.
</DynamicResponseField>
<DynamicResponseField name="included_grant" type="number">
Amount granted from the plan's included usage.
</DynamicResponseField>
<DynamicResponseField name="prepaid_grant" type="number">
Amount granted from prepaid purchases or top-ups.
</DynamicResponseField>
<DynamicResponseField name="remaining" type="number">
Remaining balance available for use.
</DynamicResponseField>
<DynamicResponseField name="usage" type="number">
Amount consumed in the current period.
</DynamicResponseField>
<DynamicResponseField name="unlimited" type="boolean">
Whether this balance has unlimited usage.
</DynamicResponseField>
<DynamicResponseField name="reset" type="object | null">
Reset configuration for this balance, or null if no reset.
<Expandable title="properties">
<DynamicResponseField name="interval" type="'one_off' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals.
</DynamicResponseField>
<DynamicResponseField name="interval_count" type="number">
Number of intervals between resets (eg. 2 for bi-monthly).
</DynamicResponseField>
<DynamicResponseField name="resets_at" type="number | null">
Timestamp when the balance will next reset.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="price" type="object | null">
Pricing configuration if this balance has usage-based pricing.
<Expandable title="properties">
<DynamicResponseField name="amount" type="number">
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
How tiers are applied: graduated (split across bands) or volume (flat rate for the matched tier).
</DynamicResponseField>
<DynamicResponseField name="billing_units" type="number">
The number of units per billing increment (eg. $9 / 250 units).
</DynamicResponseField>
<DynamicResponseField name="billing_method" type="'prepaid' | 'usage_based'">
Whether usage is prepaid or billed pay-per-use.
</DynamicResponseField>
<DynamicResponseField name="max_purchase" type="number | null">
Maximum quantity that can be purchased, or null for unlimited.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="expires_at" type="number | null">
Timestamp when this balance expires, or null for no expiration.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="rollovers" type="object[]">
Rollover balances carried over from previous periods.
<Expandable title="properties">
<DynamicResponseField name="balance" type="number">
Amount of balance rolled over from a previous period.
</DynamicResponseField>
<DynamicResponseField name="expires_at" type="number">
Timestamp when the rollover balance expires.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="balances.{key}" type="object | null">
Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature.
<Expandable title="properties">
<DynamicResponseField name="feature_id" type="string">
The feature ID this balance is for.
</DynamicResponseField>
<DynamicResponseField name="feature" type="object">
The full feature object if expanded.
<Expandable title="properties">
<DynamicResponseField name="id" type="string">
The unique identifier for this feature, used in /check and /track calls.
</DynamicResponseField>
<DynamicResponseField name="name" type="string">
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage).
</DynamicResponseField>
<DynamicResponseField name="event_names" type="string[]">
Event names that trigger this feature's balance. Allows multiple features to respond to a single event.
</DynamicResponseField>
<DynamicResponseField name="credit_schema" type="object[]">
For credit_system features: maps metered features to their credit costs.
<Expandable title="properties">
<DynamicResponseField name="metered_feature_id" type="string">
ID of the metered feature that draws from this credit system.
</DynamicResponseField>
<DynamicResponseField name="credit_cost" type="number">
Credits consumed per unit of the metered feature.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
<DynamicResponseField name="singular" type="string | null">
Singular form for UI display (e.g., 'API call', 'seat').
</DynamicResponseField>
<DynamicResponseField name="plural" type="string | null">
Plural form for UI display (e.g., 'API calls', 'seats').
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="archived" type="boolean">
Whether the feature is archived and hidden from the dashboard.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="granted" type="number">
Total balance granted (included + prepaid).
</DynamicResponseField>
<DynamicResponseField name="remaining" type="number">
Remaining balance available for use.
</DynamicResponseField>
<DynamicResponseField name="usage" type="number">
Total usage consumed in the current period.
</DynamicResponseField>
<DynamicResponseField name="unlimited" type="boolean">
Whether this feature has unlimited usage.
</DynamicResponseField>
<DynamicResponseField name="overage_allowed" type="boolean">
Whether usage beyond the granted balance is allowed (with overage charges).
</DynamicResponseField>
<DynamicResponseField name="max_purchase" type="number | null">
Maximum quantity that can be purchased as a top-up, or null for unlimited.
</DynamicResponseField>
<DynamicResponseField name="next_reset_at" type="number | null">
Timestamp when the balance will reset, or null for no reset.
</DynamicResponseField>
<DynamicResponseField name="breakdown" type="object[]">
Detailed breakdown of balance sources when stacking multiple plans or grants.
<Expandable title="properties">
<DynamicResponseField name="id" type="string">
The unique identifier for this balance breakdown.
</DynamicResponseField>
<DynamicResponseField name="plan_id" type="string | null">
The plan ID this balance originates from, or null for standalone balances.
</DynamicResponseField>
<DynamicResponseField name="included_grant" type="number">
Amount granted from the plan's included usage.
</DynamicResponseField>
<DynamicResponseField name="prepaid_grant" type="number">
Amount granted from prepaid purchases or top-ups.
</DynamicResponseField>
<DynamicResponseField name="remaining" type="number">
Remaining balance available for use.
</DynamicResponseField>
<DynamicResponseField name="usage" type="number">
Amount consumed in the current period.
</DynamicResponseField>
<DynamicResponseField name="unlimited" type="boolean">
Whether this balance has unlimited usage.
</DynamicResponseField>
<DynamicResponseField name="reset" type="object | null">
Reset configuration for this balance, or null if no reset.
<Expandable title="properties">
<DynamicResponseField name="interval" type="'one_off' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals.
</DynamicResponseField>
<DynamicResponseField name="interval_count" type="number">
Number of intervals between resets (eg. 2 for bi-monthly).
</DynamicResponseField>
<DynamicResponseField name="resets_at" type="number | null">
Timestamp when the balance will next reset.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="price" type="object | null">
Pricing configuration if this balance has usage-based pricing.
<Expandable title="properties">
<DynamicResponseField name="amount" type="number">
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
How tiers are applied: graduated (split across bands) or volume (flat rate for the matched tier).
</DynamicResponseField>
<DynamicResponseField name="billing_units" type="number">
The number of units per billing increment (eg. $9 / 250 units).
</DynamicResponseField>
<DynamicResponseField name="billing_method" type="'prepaid' | 'usage_based'">
Whether usage is prepaid or billed pay-per-use.
</DynamicResponseField>
<DynamicResponseField name="max_purchase" type="number | null">
Maximum quantity that can be purchased, or null for unlimited.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="expires_at" type="number | null">
Timestamp when this balance expires, or null for no expiration.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="rollovers" type="object[]">
Rollover balances carried over from previous periods.
<Expandable title="properties">
<DynamicResponseField name="balance" type="number">
Amount of balance rolled over from a previous period.
</DynamicResponseField>
<DynamicResponseField name="expires_at" type="number">
Timestamp when the rollover balance expires.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="deductions" type="object[]">
Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling.
<Expandable title="properties">
<DynamicResponseField name="balance_id" type="string">
ID of the underlying balance row that was deducted from (customer_entitlement or rollover).
</DynamicResponseField>
<DynamicResponseField name="feature_id" type="string">
The feature this balance belongs to.
</DynamicResponseField>
<DynamicResponseField name="plan_id" type="string | null">
ID of the plan/product this balance belongs to. Null when the balance can't be attributed to a single plan (e.g. it spans multiple).
</DynamicResponseField>
<DynamicResponseField name="reset" type="object | null">
Reset configuration for the balance this deduction came from, or null if the balance doesn't reset.
<Expandable title="properties">
<DynamicResponseField name="interval" type="'one_off' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'">
The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals.
</DynamicResponseField>
<DynamicResponseField name="interval_count" type="number">
Number of intervals between resets (eg. 2 for bi-monthly).
</DynamicResponseField>
<DynamicResponseField name="resets_at" type="number | null">
Timestamp when the balance will next reset.
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="value" type="number">
Amount deducted from this balance. Positive when usage was consumed, negative when credit was restored (e.g. a refund via negative track value).
</DynamicResponseField>
</Expandable>
</DynamicResponseField>
<ResponseExample>
```json 200
{
"customer_id": "cus_123",
"value": 0.006,
"balance": {
"feature_id": "ai_credits",
"granted": 10,
"remaining": 9.994,
"usage": 0.006,
"unlimited": false,
"overage_allowed": false,
"max_purchase": null,
"next_reset_at": 1773851121437,
"breakdown": [
{
"id": "cus_ent_39qmLooixXLAqMywgXywjAz96rV",
"plan_id": "pro_plan",
"included_grant": 100,
"prepaid_grant": 0,
"remaining": 72,
"usage": 28,
"unlimited": false,
"reset": {
"interval": "month",
"resets_at": 1773851121437
},
"price": null,
"expires_at": null
}
]
},
"deductions": [
{
"balance_id": "cus_ent_3DdSDoyFmoA9Neecl2a2Gc507X2",
"feature_id": "ai_credits",
"plan_id": "pro",
"reset": {
"interval": "month",
"resets_at": 1781288736881
},
"value": 0.006
}
]
}
```
</ResponseExample>

View File

@@ -252,7 +252,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -320,8 +320,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -603,7 +611,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -671,8 +679,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -856,8 +872,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -882,6 +898,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -987,8 +1027,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -1064,8 +1112,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -1090,6 +1138,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -406,7 +406,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -474,8 +474,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -757,7 +765,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -825,8 +833,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -1010,8 +1026,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -1036,6 +1052,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -1141,8 +1181,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -1218,8 +1266,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -1244,6 +1292,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -277,7 +277,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -345,8 +345,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -628,7 +636,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -696,8 +704,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -881,8 +897,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -907,6 +923,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -1012,8 +1052,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -1089,8 +1137,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -1115,6 +1163,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -394,7 +394,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -462,8 +462,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -745,7 +753,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -813,8 +821,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -998,8 +1014,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -1024,6 +1040,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -1129,8 +1169,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -1206,8 +1254,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -1232,6 +1280,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -359,7 +359,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -427,8 +427,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -709,7 +717,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -777,8 +785,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -961,8 +977,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -987,6 +1003,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -1092,8 +1132,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -1168,8 +1216,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -1194,6 +1242,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -131,7 +131,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -199,8 +199,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -481,7 +489,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -549,8 +557,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -733,8 +749,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -759,6 +775,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -864,8 +904,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -940,8 +988,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -966,6 +1014,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -160,7 +160,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -228,8 +228,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -510,7 +518,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -578,8 +586,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -762,8 +778,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -788,6 +804,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -893,8 +933,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -969,8 +1017,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -995,6 +1043,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -195,7 +195,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -263,8 +263,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -545,7 +553,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -613,8 +621,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />
@@ -797,8 +813,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -823,6 +839,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">
@@ -928,8 +968,16 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The per-unit price amount.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration if applicable.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'">
@@ -1004,8 +1052,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -1030,6 +1078,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -13,7 +13,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicParamField>
<DynamicParamField body="type" type="'boolean' | 'metered' | 'credit_system'" required>
<DynamicParamField body="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'" required>
The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system.
</DynamicParamField>
@@ -32,7 +32,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="credit_schema" type="object[]">
A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features.
A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead.
<Expandable title="properties">
<DynamicParamField body="metered_feature_id" type="string" required />
@@ -41,6 +41,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicParamField>
<DynamicParamField body="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration.
<Expandable title="properties">
<DynamicParamField body="markup" type="number" />
<DynamicParamField body="input_cost" type="number" />
<DynamicParamField body="output_cost" type="number" />
</Expandable>
</DynamicParamField>
<DynamicParamField body="default_markup" type="number">
Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free.
</DynamicParamField>
<DynamicParamField body="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id.
<Expandable title="properties">
<DynamicParamField body="markup" type="number" required />
</Expandable>
</DynamicParamField>
<DynamicParamField body="event_names" type="string[]" />
<DynamicParamField body="feature_id" type="string" required>
@@ -58,8 +82,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -84,6 +108,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -24,8 +24,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -50,6 +50,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -20,8 +20,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -46,6 +46,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -13,7 +13,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
The name of the feature.
</DynamicParamField>
<DynamicParamField body="type" type="'boolean' | 'metered' | 'credit_system'">
<DynamicParamField body="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system.
</DynamicParamField>
@@ -32,7 +32,7 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</DynamicParamField>
<DynamicParamField body="credit_schema" type="object[]">
A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features.
A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead.
<Expandable title="properties">
<DynamicParamField body="metered_feature_id" type="string" required />
@@ -41,6 +41,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicParamField>
<DynamicParamField body="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration.
<Expandable title="properties">
<DynamicParamField body="markup" type="number" />
<DynamicParamField body="input_cost" type="number" />
<DynamicParamField body="output_cost" type="number" />
</Expandable>
</DynamicParamField>
<DynamicParamField body="default_markup" type="number">
Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free.
</DynamicParamField>
<DynamicParamField body="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id.
<Expandable title="properties">
<DynamicParamField body="markup" type="number" required />
</Expandable>
</DynamicParamField>
<DynamicParamField body="event_names" type="string[]" />
<DynamicParamField body="archived" type="boolean">
@@ -66,8 +90,8 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
Human-readable name displayed in the dashboard and billing UI.
</DynamicResponseField>
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools.
<DynamicResponseField name="type" type="'boolean' | 'metered' | 'credit_system' | 'ai_credit_system'">
Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing.
</DynamicResponseField>
<DynamicResponseField name="consumable" type="boolean">
@@ -92,6 +116,30 @@ import { DynamicResponseExample } from "/snippets/dynamic-response-example.jsx";
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="model_markups.{key}" type="object | null">
Per-model markup overrides for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
<DynamicResponseField name="input_cost" type="number" />
<DynamicResponseField name="output_cost" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="default_markup" type="number">
Default percentage markup for AI credit systems. Use -100 to make usage free.
</DynamicResponseField>
<DynamicResponseField name="provider_markups.{key}" type="object | null">
Per-provider default markup percentages for AI credit systems.
<Expandable title="properties">
<DynamicResponseField name="markup" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="display" type="object">
Display names for the feature in billing UI and customer-facing components.
<Expandable title="properties">

View File

@@ -248,8 +248,8 @@ await autumn.plans.create({
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -409,7 +409,7 @@ await autumn.plans.create({
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -477,8 +477,16 @@ await autumn.plans.create({
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />

View File

@@ -119,7 +119,7 @@ const plan = await autumn.plans.get({
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -187,8 +187,16 @@ const plan = await autumn.plans.get({
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />

View File

@@ -136,7 +136,7 @@ const plans = await autumn.plans.list({
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -204,8 +204,16 @@ const plans = await autumn.plans.list({
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />

View File

@@ -175,8 +175,8 @@ await autumn.plans.update({
'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go.
</DynamicParamField>
<DynamicParamField body="max_purchase" type="number">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total.
<DynamicParamField body="max_purchase" type="number | null">
Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit.
</DynamicParamField>
</Expandable>
@@ -263,6 +263,8 @@ await autumn.plans.update({
The new ID to use for the plan. Can only be updated if the plan has not been used by any customers.
</DynamicParamField>
<DynamicParamField body="disable_version" type="boolean" />
### Response
@@ -344,7 +346,7 @@ await autumn.plans.update({
The name of the feature.
</DynamicResponseField>
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system'">
<DynamicResponseField name="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'">
The type of the feature
</DynamicResponseField>
@@ -412,8 +414,16 @@ await autumn.plans.update({
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</DynamicResponseField>
<DynamicResponseField name="tiers" type="any[]">
<DynamicResponseField name="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<DynamicResponseField name="to" type="number" />
<DynamicResponseField name="amount" type="number" />
<DynamicResponseField name="flat_amount" type="number" />
</Expandable>
</DynamicResponseField>
<DynamicResponseField name="tier_behavior" type="'graduated' | 'volume'" />

View File

@@ -97,6 +97,168 @@ openapi: "api/openapi.yml webhook billing.updated"
The ID of the feature that was added or removed.
</ParamField>
<ParamField body="item" type="object" required>
The item snapshot that was added or removed.
<Expandable title="properties">
<ParamField body="feature_id" type="string" required>
The ID of the feature this item configures.
</ParamField>
<ParamField body="feature" type="object">
The full feature object if expanded.
<Expandable title="properties">
<ParamField body="id" type="string" required>
The ID of the feature, used to refer to it in other API calls like /track or /check.
</ParamField>
<ParamField body="name" type="string | null">
The name of the feature.
</ParamField>
<ParamField body="type" type="'static' | 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'ai_credit_system'" required>
The type of the feature
</ParamField>
<ParamField body="display" type="object | null">
Singular and plural display names for the feature.
<Expandable title="properties">
<ParamField body="singular" type="string" required>
The singular display name for the feature.
</ParamField>
<ParamField body="plural" type="string" required>
The plural display name for the feature.
</ParamField>
</Expandable>
</ParamField>
<ParamField body="credit_schema" type="object[] | null">
Credit cost schema for credit system features.
<Expandable title="properties">
<ParamField body="metered_feature_id" type="string" required>
The ID of the metered feature (should be a single_use feature).
</ParamField>
<ParamField body="credit_cost" type="number" required>
The credit cost of the metered feature.
</ParamField>
</Expandable>
</ParamField>
<ParamField body="archived" type="boolean | null">
Whether or not the feature is archived.
</ParamField>
</Expandable>
</ParamField>
<ParamField body="included" type="number" required>
Number of free units included. For consumable features, balance resets to this number each interval.
</ParamField>
<ParamField body="unlimited" type="boolean" required>
Whether the customer has unlimited access to this feature.
</ParamField>
<ParamField body="reset" type="object | null" required>
Reset configuration for consumable features. Null for non-consumable features like seats where usage persists across billing cycles.
<Expandable title="properties">
<ParamField body="interval" type="'one_off' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'" required>
The interval at which the feature balance resets (e.g. 'month', 'year'). For consumable features, usage resets to 0 and included units are restored.
</ParamField>
<ParamField body="interval_count" type="number">
Number of intervals between resets. Defaults to 1.
</ParamField>
</Expandable>
</ParamField>
<ParamField body="price" type="object | null" required>
Pricing configuration for usage beyond included units. Null if feature is entirely free.
<Expandable title="properties">
<ParamField body="amount" type="number">
Price per billing_units after included usage is consumed. Mutually exclusive with tiers.
</ParamField>
<ParamField body="tiers" type="object[]">
Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required.
<Expandable title="properties">
<ParamField body="to" type="number" required />
<ParamField body="amount" type="number" required />
<ParamField body="flat_amount" type="number" />
</Expandable>
</ParamField>
<ParamField body="tier_behavior" type="'graduated' | 'volume'" />
<ParamField body="interval" type="'one_off' | 'week' | 'month' | 'quarter' | 'semi_annual' | 'year'" required>
Billing interval for this price. For consumable features, should match reset.interval.
</ParamField>
<ParamField body="interval_count" type="number">
Number of intervals per billing cycle. Defaults to 1.
</ParamField>
<ParamField body="billing_units" type="number" required>
Number of units per price increment. Usage is rounded UP to the nearest billing_units when billed (e.g. billing_units=100 means 101 usage rounds to 200).
</ParamField>
<ParamField body="billing_method" type="'prepaid' | 'usage_based'" required>
'prepaid' for features like seats where customers pay upfront, 'usage_based' for pay-as-you-go after included usage.
</ParamField>
<ParamField body="max_purchase" type="number | null" required>
Maximum units a customer can purchase beyond included. E.g. if included=100 and max_purchase=300, customer can use up to 400 total before usage is capped. Null for no limit.
</ParamField>
</Expandable>
</ParamField>
<ParamField body="display" type="object">
Display text for showing this item in pricing pages.
<Expandable title="properties">
<ParamField body="primary_text" type="string" required>
Main display text (e.g. '$10' or '100 messages').
</ParamField>
<ParamField body="secondary_text" type="string">
Secondary display text (e.g. 'per month' or 'then $0.5 per 100').
</ParamField>
</Expandable>
</ParamField>
<ParamField body="rollover" type="object">
Rollover configuration for unused units. If set, unused included units roll over to the next period.
<Expandable title="properties">
<ParamField body="max" type="number | null" required>
Maximum rollover units. Null for unlimited rollover.
</ParamField>
<ParamField body="max_percentage" type="number | null">
Maximum rollover as a percentage (0-100) of included + prepaid grant. Mutually exclusive with max.
</ParamField>
<ParamField body="expiry_duration_type" type="'month' | 'forever'" required>
When rolled over units expire.
</ParamField>
<ParamField body="expiry_duration_length" type="number">
Number of periods before expiry.
</ParamField>
</Expandable>
</ParamField>
</Expandable>
</ParamField>
</Expandable>
</ParamField>

File diff suppressed because it is too large Load Diff

View File

@@ -133,6 +133,7 @@
"documentation/slack-discord-notifications",
"documentation/fail-open",
"documentation/rate-limits",
"documentation/external-providers/ai-sdk",
"documentation/external-providers/convex",
"documentation/external-providers/revenuecat",
"documentation/external-providers/vercel-marketplace"
@@ -205,6 +206,7 @@
"pages": [
"api-reference/core/check",
"api-reference/core/track",
"api-reference/balances/trackTokens",
"api-reference/core/batchTrack",
"api-reference/balances/createBalance",
"api-reference/balances/updateBalance",

View File

@@ -131,6 +131,85 @@ curl -X POST "https://api.useautumn.com/v1/balances/update" \
can reset or override incremental usage recorded through events.
</Warning>
## Tracking AI Token Usage
If you're using an [AI credit system](/examples/monetary-credits), you can track token usage directly with `trackTokens`. This automatically converts token counts to a dollar cost using [Models.dev](https://models.dev) pricing and your configured markup, then deducts from the customer's credit balance.
The `modelId` must be in `provider/model` format, matching the provider and model keys from [Models.dev](https://models.dev). For example:
- `anthropic/claude-sonnet-4-5-20250514`
- `openai/gpt-4o`
- `google/gemini-2.5-pro`
For providers with nested model paths (like OpenRouter), include the full path after the provider: `openrouter/anthropic/claude-opus-4.6`.
Token counts are **exclusive pools**: `inputTokens` should exclude cached tokens (pass those as `cacheReadTokens` / `cacheWriteTokens`) and `outputTokens` should exclude reasoning tokens (pass those as `reasoningTokens`). Audio tokens go in `audioInputTokens` / `audioOutputTokens`. See the [API reference](/api-reference/balances/trackTokens) for the full parameter list.
<Note>
`autumn.balances.trackTokens` requires an autumn-js release that includes
the method. On older versions, call the REST endpoint directly — see the
cURL tab below.
</Note>
<CodeGroup>
```typescript TypeScript
import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
await autumn.balances.trackTokens({
customerId: "user_123",
modelId: "anthropic/claude-opus-4-6",
inputTokens: 1000,
outputTokens: 500,
});
```
```python Python
from autumn_sdk import Autumn
autumn = Autumn("am_sk_test_1234")
await autumn.balances.track_tokens(
customer_id="user_123",
model_id="anthropic/claude-opus-4-6",
input_tokens=1000,
output_tokens=500,
)
```
```bash cURL
curl -X POST "https://api.useautumn.com/v1/balances.track_tokens" \
-H "Authorization: Bearer am_sk_test_1234" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_123",
"model_id": "anthropic/claude-opus-4-6",
"input_tokens": 1000,
"output_tokens": 500
}'
```
</CodeGroup>
<Tip>
If the customer has exactly one AI credit system feature, you can omit the
`featureId` parameter — it will be auto-detected. The request fails with an
error if the customer has no AI credit system, or has more than one and no
`featureId` is provided.
</Tip>
### Vercel AI SDK integration
If you're using the [Vercel AI SDK](https://sdk.vercel.ai), the `@useautumn/ai-sdk` package can automatically track token usage for every `generateText` or `streamText` call — no manual `trackTokens` calls needed.
<Card
title="Vercel AI SDK Integration"
horizontal
href="/documentation/external-providers/ai-sdk"
icon="wand-magic-sparkles"
/>
## Using Event Names
In the above examples, we used the `featureId` to identify the feature. You can instead use the `eventName` parameter to link balances to different events in your application. This can be useful when:

View File

@@ -0,0 +1,145 @@
---
title: "Vercel AI SDK"
description: "Automatically track AI token usage with the Vercel AI SDK"
---
The `@useautumn/ai-sdk` package integrates Autumn with the [Vercel AI SDK](https://sdk.vercel.ai), automatically tracking token usage for every `generateText` or `streamText` call. No manual `trackTokens` calls needed.
## Setup
#### 1. Install the package
<CodeGroup>
```bash npm
npm install @useautumn/ai-sdk
```
```bash pnpm
pnpm add @useautumn/ai-sdk
```
```bash yarn
yarn add @useautumn/ai-sdk
```
```bash bun
bun add @useautumn/ai-sdk
```
</CodeGroup>
<Note>
Requires `autumn-js` and `ai` (v6+) as peer dependencies.
</Note>
#### 2. Wrap your model
Use `withAutumn` to wrap any AI SDK language model. It intercepts generate and stream calls, reads the token usage from the response, and reports it to Autumn automatically.
```typescript
import { Autumn } from "autumn-js";
import { anthropic } from "@ai-sdk/anthropic";
import { withAutumn } from "@useautumn/ai-sdk";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
const model = withAutumn({
autumn,
model: anthropic("claude-sonnet-4-5-20250514"),
customerId: "user_123",
});
```
#### 3. Use as normal
The wrapped model works exactly like a regular AI SDK model. Token usage is tracked in the background after each call.
```typescript
import { generateText, streamText } from "ai";
// Generate — usage tracked automatically
const { text } = await generateText({
model,
prompt: "Explain quantum computing in one paragraph",
});
// Stream — usage tracked when the stream finishes
const result = streamText({
model,
prompt: "Write a short poem about recursion",
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
```
## Token pools
The wrapper normalizes the AI SDK's usage object into the exclusive token pools that [trackTokens](/api-reference/balances/trackTokens) expects: text input (excluding cached tokens), text output (excluding reasoning tokens), cache reads, cache writes, and reasoning tokens. Each pool is billed at the model's published rate, so cached and reasoning-heavy requests are priced correctly without any extra work.
## Model ID format
The wrapped model constructs the `modelId` sent to Autumn using `provider/model` format, derived from the AI SDK model's `provider` and `modelId` fields. This must match a valid provider and model key from [Models.dev](https://models.dev).
For example:
- `@ai-sdk/anthropic` → `anthropic/claude-sonnet-4-5-20250514`
- `@ai-sdk/openai` → `openai/gpt-4o`
- `@ai-sdk/google` → `google/gemini-2.5-pro`
If the AI SDK provider name doesn't match the Models.dev provider key, use the `providerId` option to override it:
```typescript
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
const openrouter = createOpenRouter();
const model = withAutumn({
autumn,
model: openrouter("anthropic/claude-opus-4.6"),
customerId: "user_123",
providerId: "openrouter", // Override provider prefix
});
// Sends modelId as "openrouter/anthropic/claude-opus-4.6"
```
## Options
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `autumn` | `Autumn` | Yes | Your Autumn SDK client instance |
| `model` | `LanguageModelV3` | Yes | The AI SDK language model to wrap |
| `customerId` | `string` | Yes | The Autumn customer ID to attribute usage to |
| `providerId` | `string` | No | Override the provider prefix in the model name sent to Autumn. Falls back to the model's `provider` field |
| `featureId` | `string` | No | Target a specific AI credit system feature. Auto-detected if you only have one |
| `entityId` | `string` | No | Entity ID for entity-scoped balance tracking |
| `properties` | `Record<string, unknown>` | No | Additional properties to attach to each usage event |
## Full example
```typescript
import { Autumn } from "autumn-js";
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { withAutumn } from "@useautumn/ai-sdk";
const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY! });
async function chat(customerId: string, message: string) {
const model = withAutumn({
autumn,
model: openai("gpt-4o"),
customerId,
});
const { text } = await generateText({
model,
prompt: message,
});
return text;
}
```
<Tip>
Tracking failures are caught and logged to the console — they won't break your AI features. Check your server logs if usage isn't appearing in Autumn.
</Tip>

View File

@@ -271,3 +271,137 @@ See the credits pricing guide for a more detailed example of setting up a moneta
href="/examples/monetary-credits"
icon="money-bills"
/>
## AI Credit Systems
For AI applications that need to track token usage with per-model pricing, you can create an AI credit system. This lets you define markup percentages for each model and automatically calculate costs based on input/output tokens.
<Tabs>
<Tab title="CLI">
Markups are optional. `defaultMarkup` applies to every model unless overridden — by `providerMarkups` (keyed by the first segment of the model ID, e.g. `openrouter`), or by `modelMarkups` for a specific model, which takes highest priority. With no markups set, models are billed at their Models.dev base cost.
A markup of `-100` makes the model free: usage events are still recorded, but nothing is deducted from the balance.
```ts Simplest setup — one markup for everything
export const aiCredits = feature({
id: 'ai_credits',
name: 'AI Credits',
type: 'ai_credit_system',
defaultMarkup: 30, // every model billed at models.dev cost + 30%
});
```
Or mix the levels for finer control:
```ts autumn.config.ts
import { feature, item, plan } from 'atmn';
export const aiCredits = feature({
id: 'ai_credits',
name: 'AI Credits',
type: 'ai_credit_system',
// Global fallback markup
defaultMarkup: 30,
// Per-provider defaults
providerMarkups: {
openrouter: { markup: 25 },
},
// Per-model overrides (highest priority)
modelMarkups: {
'anthropic/claude-opus-4-5': { markup: 20 },
'anthropic/claude-sonnet-4-5': { markup: 15 },
'openai/gpt-4o-mini': { markup: -100 }, // free for customers
// For custom/self-hosted models, specify input/output costs in $/M tokens
'custom/my-model': { markup: 25, inputCost: 0.01, outputCost: 0.03 },
},
});
export const pro = plan({
id: 'pro',
name: 'Pro',
price: { amount: 50, interval: 'month' },
items: [
item({
featureId: aiCredits.id,
included: 10, // $10 worth of AI credits
reset: { interval: 'month' },
}),
],
});
```
Push changes with `atmn push`.
</Tab>
<Tab title="Dashboard">
1. Navigate to the features page, under Plans.
2. Click "Create Credit System"
3. Toggle "AI Credit System" to enable model-based pricing
4. Set a default markup %, and optionally add providers with their own default markups
5. Add the models you want to support, overriding the markup per model where needed
6. For custom models, also specify input/output costs per million tokens
7. Click "Create"
</Tab>
</Tabs>
### Model ID Format
Model IDs follow the `provider/model` format:
- Standard models: `anthropic/claude-opus-4-5`, `openai/gpt-4o`
- OpenRouter models: `openrouter/anthropic/claude-opus-4.6`
- Custom models: `custom/my-model-name`
For standard models, pricing is automatically fetched from models.dev, including separate rates for cache reads/writes, reasoning, and audio tokens where the model publishes them, plus large-context tier pricing (e.g. above 200k input tokens) when applicable.
For custom models, you must specify both `inputCost` and `outputCost` in dollars per million tokens — tracking fails if either is missing. Custom models bill input and output tokens only; cache, reasoning, and audio pools are ignored.
### Tracking Token Usage
Use the `trackTokens` endpoint to deduct credits based on token usage:
<CodeGroup>
```typescript TypeScript
import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
await autumn.balances.trackTokens({
customerId: "user_123",
modelId: "anthropic/claude-opus-4-5",
inputTokens: 1500,
outputTokens: 500,
});
```
```python Python
from autumn_sdk import Autumn
autumn = Autumn("am_sk_test_1234")
await autumn.balances.track_tokens(
customer_id="user_123",
model_id="anthropic/claude-opus-4-5",
input_tokens=1500,
output_tokens=500,
)
```
```bash cURL
curl -X POST "https://api.useautumn.com/v1/balances.track_tokens" \
-H "Authorization: Bearer am_sk_test_1234" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_123",
"model_id": "anthropic/claude-opus-4-5",
"input_tokens": 1500,
"output_tokens": 500
}'
```
</CodeGroup>
The cost is calculated automatically based on the model's pricing plus your configured markup percentage.

View File

@@ -25,6 +25,10 @@ Autumn enforces rate limits to ensure reliable performance for all users. Limits
When a rate limit is exceeded, the API returns a `429 Too Many Requests` response. Your application should back off and retry after the rate limit window resets.
## Service overload (503)
Under heavy load, customer-state endpoints (`/customers.get_or_create`, `/customers.get`, `/entities.get`) may briefly return a `503` with code `service_unavailable` and a `Retry-After` header (seconds). This is transient and unrelated to your request volume -- retry after the indicated delay. `check` and `track` are never shed this way (see [Fail-Open Defaults](/documentation/fail-open)).
## Preview endpoints are not rate limited
Preview endpoints like `/v1/attach/preview`, `/v1/billing.preview_attach`, and `/v1/billing.preview_update` are **not** subject to rate limits. You can call these freely to display pricing previews to your users.

View File

@@ -105,12 +105,12 @@ export default async function BlogPostPage({ params }: { params: BlogParams }) {
</header>
{post.image && (
<div className="relative w-full aspect-[2/1] overflow-hidden border border-[#292929] mb-12">
<div className="relative w-full aspect-[2/1] overflow-hidden border border-[#292929] bg-[#080808] mb-12">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover"
className="object-contain"
priority
sizes="(max-width: 768px) 100vw, 720px"
/>

View File

@@ -0,0 +1,124 @@
---
title: "Active-active fixed our counters and broke everything else"
description: "Why we moved Autumn to a multi-region architecture, the tradeoffs we hit with active-active Redis, and why we eventually returned to a simpler single-region setup."
date: "2026-06-08"
author: "John, Autumn Co-Founder"
slug: "active-active-redis-cache"
image: "/images/blog/multi-region-initial-architecture.png"
---
Last year we started to onboard companies with a global customer base. With our own users starting to appear in more regions, we decided to build a multi-region architecture to reduce latency times globally.
Initially, our services were isolated to one region, us-west.
Our aim was to reduce latency in two regions to start, us-west and us-east, and targeted a round trip latency of under 50ms. The main difficulty was that this applied to both reads and writes, so simply using DB read replicas werent an option. Ultimately, there were two major considerations:
- How to spin up our server in multiple regions
- More crucially though, how to make data reads and writes low latency across regions
## Spinning up our server in multiple regions
There were two options here. Either we went serverless with something like Cloudflare Workers, or we manually spun up stateful servers in different regions. We went with the latter for a couple reasons:
1. The whole point of this was to reduce latency. With serverless, we were afraid of inconsistent latencies due to cold startup times, which we benchmarked and proved to be true.
2. Our server was already stateful, and going serverless wouldve broken patterns we relied on. Event batching, for one, gets painful when every request runs in an isolated session.
This blog from [Unkey](https://www.unkey.com/blog/serverless-exit) was really helpful when we made our decision. Now our next challenge was deciding on a provider. Our requirements were simple:
- Latency should be as low as possible
- Spinning up multi-region servers should be as simple as possible
Surprisingly, we tried almost every provider we could find and none of them fit perfectly. We ultimately chose AWS ECS, managed through Flightcontrol, where we spun up an ECS service in us-west and us-east, then used Route53 to route requests based on region.
![Route53 ECS regional routing](/images/blog/multi-region-route53-ecs-regional-routing.png)
To explain why we came to this decision, its worth walking through the other top contenders.
**[Render](https://render.com/)**
We were originally on Render so this seemed like the obvious choice. However, Render doesnt natively support multi-region, so to set this up we had to manually create instances in each region. More annoyingly though, the only way to have a single domain route to different instances was to use Cloudflares load balancer.
![Render Cloudflare load balancer](/images/blog/multi-region-render-cloudflare-load-balancer.png)
Ultimately, we chose AWS over Render because we found that Cloudflare's Load Balancer introduced additional latency compared to Route53, which resolved at the DNS layer. With Render, there were also multiple hops involved as Render itself uses Cloudflare in front of their services.
**[Railway](https://railway.com/)**
Railway was extremely compelling because they supported multi-region natively. That meant that you could spin up a single service, have it replicated across different regions, and they would handle load balancing, provisioning, and more for you. The DX was unmatched. Unfortunately though, Railways infra isnt on AWS. They build their own machines. This means a couple things:
- Our database, cache, and other data stores wouldnt be co-located with our server, unless we used Railway for those as well, which was too limiting for us
- Most of our users were also hosted on AWS so their servers wouldnt be as close to ours
![Railway AWS data hop](/images/blog/multi-region-railway-aws-data-hop.png)
Ultimately, with both providers, the decision came down to latency. AWS consistently provided the lowest latencies in our benchmarks.
![Provider p99 Checkly benchmark](/images/blog/multi-region-provider-p99-checkly-benchmark.svg)
That said, ECS came with a bunch of maintenance overhead, especially coming from Render. Even with Flightcontrol, we had to build an internal dashboard to build and deploy across regions at once. Moreover, application and load balancer logs were an absolute pain to set up. But today Im very glad we made the tradeoff. Having lower-level control over our infra has been useful, and AI has made things much easier too.
## Making data reads and writes multi-region
The bigger challenge we faced was with data access: making both reads and writes fast across regions. Think of us as a complex rate limiter. Before a request is allowed through, we often need to update usage counters atomically and decide whether the customer still has access.
For example, when you send a message to Cursor, they may deduct an estimated number of credits before accepting your message, then reconcile the actual usage afterwards. Since these writes sit on the hot path, they need to be real-time and fast. We considered several approaches to solving this.
1. **A master database per region**
Wed spin up a Postgres database in each region, completely isolated from each other, and let our users pick which region their data lives in, so it sits closest to their server. The catch, beyond running multiple databases, is that our users own customers might be spread across regions. For example, if theyre running Cloudflare Workers, pinning a whole account to one region doesnt hold up.
![Master database per region users](/images/blog/multi-region-master-db-per-region-users.png)
2. **A region per customer**
Instead of pinning our user, we could pin a customer: our users user. Each customer is tied to a region, and all their reads and writes happen there. We'd keep a record mapping customers to regions, and route each request accordingly.
![Region per customer](/images/blog/multi-region-region-per-customer.png)
Now trying to do this with Postgres sounded like a headache. Imagine trying to JOIN data across different databases. We could simplify this with a read/write cache in each region instead of fully separate Postgres databases, but we still ruled it out because of the routing layer. We'd need yet another cache for the customer-to-region mapping, itself replicated across regions, and getting every request to the right region felt like way too much overhead.
3. **Active-active Redis database**
The final approach, which we ended up going with, was using an Active-Active database from Redis Cloud. You spin up Redis caches in multiple regions, all fully synced, and you can write to any of them. When concurrent writes hit the same key in different regions, Redis Cloud resolves the conflict using CRDTs: Conflict-free Replicated Data Types.
Using a counter as an example: two concurrent increment operations merge into their sum rather than overwriting each other. This fit our use case perfectly. Each server connects to its own Redis cache in the cluster, and since our writes are just increments, the conflicts get resolved for us.
## Why we went back
We chose the Active-Active Redis database for simplicity, and while it definitely created the least infra overhead, I think it wasnt really the right solution for us, which led to more complexity than it was worth.
1. **Race conditions**
First of all, with the active-active database, even though it solved that counter case perfectly, we found ourselves running into a bunch of race conditions. Take the following example:
- We store each customer as a JSON blob with `customer_id` as the key
- Your customer performs an upgrade on us-east so we append to their `subscriptions` array
- At the same time, Stripe sends an `invoice.paid` webhook to our us-west server and we append to the customers `invoices` array
Now, both of these append operations happen on the same key and are done via a read-update-set operation. Since they happen in different regions, Redis resolves the conflict through a Last-Write-Win strategy. So either the invoices or subscriptions array will be missing an item.
To solve these types of issues, wed often have to normalize the data. For instance, we might store the subscriptions and invoices array as separate keys, `customer_id:subscriptions` and `customer_id:invoices`. Ultimately though, we ran into these issues more often than wed hoped, especially since it was hard to replicate a multi-region setup locally.
2. **Infra overhead**
The second issue we kept running into was infra overhead. It wasnt just slowing us down; it was starting to affect reliability too.
A couple of months ago, we had a user run a cron job every hour that spiked our Redis CPU and degraded the server. The quick fix wouldve been to spin up a separate Redis database for that user, so their load wouldnt impact everyone else. But because of our multi-region architecture, what should have been a simple isolation fix became much more complex and delayed.
Reliability matters more to us than latency. So when our architecture made it harder to ship reliability fixes quickly, that was a strong signal that the tradeoff no longer made sense.
Ultimately, the thing that pushed us to move back to a single-region architecture was noticing that traffic was split roughly 95:5 between us-east and us-west. Taking on all of that complexity and giving up speed and reliability for this small slice of traffic didnt feel worth it.
## Conclusion
Ever since weve moved back to a single-region architecture, weve been way more confident in our infra and reliability, and have been able to make changes, introduce new services, and ship features way faster too. Focusing on optimizing a smaller scope has felt like a huge difference. So generally, were very happy about our decision. Now, two concluding thoughts:
**Dont “move fast and break things” with infra**
I think the mistake we made with our multi-region setup was optimizing for simplicity and speed rather than choosing the architecture that would hold up best long term. Infra is a little counterintuitive to the usual “ship fast” startup advice. These decisions affect reliability directly, and theyre often some of the hardest decisions to reverse later. So while speed still matters, infra choices deserve more upfront thought than your average product decision.
**The “smart” choice isnt always the best one**
With our original approach, I think we convinced ourselves that an Active-Active Redis database would be a silver bullet, and that choosing it was the “smart” move. But infra is all about tradeoffs. Theres a reason writable database replicas arent common: they add a lot of complexity, and that complexity has to show up somewhere.
Well definitely go back to multi-region at some point. But when we do, I think well take a “less hacky” approach: route each customer to a single home region, and keep their data and traffic there. Its much easier to reason about, and probably a lot more reliable.

View File

@@ -20,6 +20,20 @@ const nextConfig = {
// falling back to WebP. Next.js negotiates via Accept header automatically.
formats: ["image/avif", "image/webp"],
},
async redirects() {
return [
{
source: "/docs",
destination: "https://docs.useautumn.com",
permanent: false,
},
{
source: "/blog/how-we-built-a-multi-region-architecture-and-why-we-went-back",
destination: "/blog/active-active-redis-cache",
permanent: true,
},
];
},
async headers() {
if (!isProd) return [];

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -0,0 +1,75 @@
<svg width="2048" height="430" viewBox="0 0 2048 430" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="2048" height="430" fill="#FFFFFF"/>
<rect x="0" y="0" width="2048" height="430" fill="#FFFFFF"/>
<line x1="34" y1="88" x2="2014" y2="88" stroke="#E2E7EE" stroke-width="1.5"/>
<line x1="34" y1="202" x2="2014" y2="202" stroke="#E2E7EE" stroke-width="1.5"/>
<line x1="34" y1="316" x2="2014" y2="316" stroke="#E2E7EE" stroke-width="1.5"/>
<text x="64" y="58" fill="#526070" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="17" font-weight="500" letter-spacing="0.2">MONITOR</text>
<text x="766" y="58" fill="#526070" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="17" font-weight="500" letter-spacing="0.2">TYPE</text>
<text x="876" y="58" fill="#526070" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="17" font-weight="500" letter-spacing="0.2">LAST 24 HRS</text>
<text x="1242" y="58" fill="#526070" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="17" font-weight="500" letter-spacing="0.2">UPTIME</text>
<text x="1386" y="58" fill="#526070" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="17" font-weight="500" letter-spacing="0.2">SUCCESS</text>
<text x="1552" y="58" fill="#526070" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="17" font-weight="500" letter-spacing="0.2">P99</text>
<text x="1680" y="58" fill="#526070" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="17" font-weight="500" letter-spacing="0.2">RESPONSE</text>
<text x="1830" y="58" fill="#526070" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="17" font-weight="500" letter-spacing="0.2">INTERVAL</text>
<g transform="translate(64 116)">
<circle cx="28" cy="28" r="28" fill="#63CE6C"/>
<path d="M17 29.5L25 37.5L41 18.5" stroke="white" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="106" y="24" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="26" font-weight="700">AWS ECS Get Customer (US East)</text>
<text x="106" y="62" fill="#718093" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="21" font-weight="400">1 minute ago</text>
<rect x="702" y="16" width="45" height="34" rx="4" fill="#263140"/>
<text x="712" y="38" fill="#FFFFFF" font-family="'SF Mono', ui-monospace, Menlo, monospace" font-size="15" font-weight="700">API</text>
<g fill="#60C96A" transform="translate(812 11)">
<rect x="0" y="30" width="7" height="35"/><rect x="18" y="22" width="7" height="43"/><rect x="36" y="42" width="7" height="23"/><rect x="54" y="18" width="7" height="47"/><rect x="72" y="54" width="7" height="11"/><rect x="90" y="32" width="7" height="33"/><rect x="108" y="12" width="7" height="53"/><rect x="126" y="36" width="7" height="29"/><rect x="144" y="45" width="7" height="20"/><rect x="162" y="38" width="7" height="27"/><rect x="180" y="29" width="7" height="36"/><rect x="198" y="44" width="7" height="21"/><rect x="216" y="52" width="7" height="13"/><rect x="234" y="26" width="7" height="39"/><rect x="252" y="48" width="7" height="17"/><rect x="270" y="42" width="7" height="23"/><rect x="288" y="18" width="7" height="47"/>
</g>
<text x="1178" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">100 %</text>
<text x="1330" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">100 %</text>
<text x="1492" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">55</text>
<text x="1526" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="18" font-weight="400"> ms</text>
<text x="1638" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">88</text>
<text x="1672" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="18" font-weight="400"> ms</text>
<text x="1788" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">1 min</text>
<circle cx="1908" cy="23" r="3.5" fill="#68778A"/><circle cx="1908" cy="39" r="3.5" fill="#68778A"/><circle cx="1908" cy="55" r="3.5" fill="#68778A"/>
</g>
<g transform="translate(64 230)">
<circle cx="28" cy="28" r="28" fill="#63CE6C"/>
<path d="M17 29.5L25 37.5L41 18.5" stroke="white" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="106" y="24" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="26" font-weight="700">Railway Get Customer (US East)</text>
<text x="106" y="62" fill="#718093" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="21" font-weight="400">less than a minute ago</text>
<rect x="702" y="16" width="45" height="34" rx="4" fill="#263140"/>
<text x="712" y="38" fill="#FFFFFF" font-family="'SF Mono', ui-monospace, Menlo, monospace" font-size="15" font-weight="700">API</text>
<g fill="#60C96A" transform="translate(812 11)">
<rect x="0" y="20" width="7" height="45"/><rect x="18" y="42" width="7" height="23"/><rect x="36" y="51" width="7" height="14"/><rect x="54" y="48" width="7" height="17"/><rect x="72" y="34" width="7" height="31"/><rect x="90" y="45" width="7" height="20"/><rect x="108" y="41" width="7" height="24"/><rect x="126" y="39" width="7" height="26"/><rect x="144" y="56" width="7" height="9"/><rect x="162" y="47" width="7" height="18"/><rect x="180" y="37" width="7" height="28"/><rect x="198" y="35" width="7" height="30"/><rect x="216" y="43" width="7" height="22"/><rect x="234" y="51" width="7" height="14"/><rect x="252" y="28" width="7" height="37"/><rect x="270" y="38" width="7" height="27"/><rect x="288" y="14" width="7" height="51"/>
</g>
<text x="1178" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">100 %</text>
<text x="1330" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">100 %</text>
<text x="1492" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">120</text>
<text x="1538" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="18" font-weight="400"> ms</text>
<text x="1638" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">178</text>
<text x="1684" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="18" font-weight="400"> ms</text>
<text x="1788" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">1 min</text>
<circle cx="1908" cy="23" r="3.5" fill="#68778A"/><circle cx="1908" cy="39" r="3.5" fill="#68778A"/><circle cx="1908" cy="55" r="3.5" fill="#68778A"/>
</g>
<g transform="translate(64 344)">
<circle cx="28" cy="28" r="28" fill="#63CE6C"/>
<path d="M17 29.5L25 37.5L41 18.5" stroke="white" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="106" y="24" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="26" font-weight="700">Render Get Customer (US East)</text>
<text x="106" y="62" fill="#718093" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="21" font-weight="400">less than a minute ago</text>
<rect x="702" y="16" width="45" height="34" rx="4" fill="#263140"/>
<text x="712" y="38" fill="#FFFFFF" font-family="'SF Mono', ui-monospace, Menlo, monospace" font-size="15" font-weight="700">API</text>
<g fill="#60C96A" transform="translate(812 11)">
<rect x="0" y="44" width="7" height="21"/><rect x="18" y="54" width="7" height="11"/><rect x="36" y="48" width="7" height="17"/><rect x="54" y="5" width="7" height="60"/><rect x="72" y="53" width="7" height="12"/><rect x="90" y="42" width="7" height="23"/><rect x="108" y="38" width="7" height="27"/><rect x="126" y="36" width="7" height="29"/><rect x="144" y="41" width="7" height="24"/><rect x="162" y="46" width="7" height="19"/><rect x="180" y="45" width="7" height="20"/><rect x="198" y="19" width="7" height="46"/><rect x="216" y="51" width="7" height="14"/><rect x="234" y="36" width="7" height="29"/><rect x="252" y="47" width="7" height="18"/><rect x="270" y="30" width="7" height="35"/><rect x="288" y="40" width="7" height="25"/>
</g>
<text x="1178" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">100 %</text>
<text x="1330" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">100 %</text>
<text x="1492" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">106</text>
<text x="1538" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="18" font-weight="400"> ms</text>
<text x="1638" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">154</text>
<text x="1684" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="18" font-weight="400"> ms</text>
<text x="1788" y="43" fill="#202632" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="25" font-weight="400">1 min</text>
<circle cx="1908" cy="23" r="3.5" fill="#68778A"/><circle cx="1908" cy="39" r="3.5" fill="#68778A"/><circle cx="1908" cy="55" r="3.5" fill="#68778A"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -182,6 +182,22 @@
"typescript": "^6.0.2",
},
},
"packages/ai-sdk": {
"name": "@useautumn/ai-sdk",
"version": "0.0.1",
"dependencies": {
"@ai-sdk/provider": "^3.0.0",
},
"devDependencies": {
"@types/node": "^24.9.1",
"tsup": "^8.4.0",
"typescript": "^5.8.3",
},
"peerDependencies": {
"ai": "^6.0.116",
"autumn-js": "*",
},
},
"packages/atmn": {
"name": "atmn",
"version": "1.1.8",
@@ -666,13 +682,11 @@
"unrs-resolver",
],
"overrides": {
"@better-auth/core": "1.6.5",
"@better-auth/passkey": "1.6.5",
"@isaacs/brace-expansion": "5.0.1",
"@modelcontextprotocol/sdk": "1.29.0",
"@smithy/config-resolver": "^4.4.0",
"@types/pg": "8.20.0",
"better-auth": "1.6.5",
"diff": "8.0.3",
"esbuild": "0.25.0",
"fast-xml-parser": "5.3.4",
@@ -2780,6 +2794,8 @@
"@upstash/redis": ["@upstash/redis@1.38.0", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg=="],
"@useautumn/ai-sdk": ["@useautumn/ai-sdk@workspace:packages/ai-sdk"],
"@useautumn/sdk": ["@useautumn/sdk@workspace:packages/sdk"],
"@vdemedes/prettier-config": ["@vdemedes/prettier-config@2.0.1", "", {}, "sha512-lcHyyLfS2ro282qsXKpxw+canUkOlFIGoanxt3BaNCm5K1NR8k4hGvYbFO54/+QWq12d0y/EYRz68yNQkqWFrw=="],
@@ -6340,6 +6356,10 @@
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@better-auth/cli/@better-auth/core": ["@better-auth/core@1.4.21", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-R4s7pwShkqB21fZ599QASbXxqFcoxanLyz7DHSX6SJPNYV748wBLsm3xM9VrjfvWMpS+cQUErOCt9yWT1hMn6w=="],
"@better-auth/cli/better-auth": ["better-auth@1.4.21", "", { "dependencies": { "@better-auth/core": "1.4.21", "@better-auth/telemetry": "1.4.21", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-qdrIZS7xnGF2HPBV5wYNPWTkPojhauOOjz1+MhLvwFy+zXpgLofQmWsI5I9DY+ef845NKt93XcgpyAc4RPPT9A=="],
"@better-auth/cli/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"@better-auth/cli/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
@@ -7122,6 +7142,8 @@
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"@useautumn/ai-sdk/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@useautumn/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@vercel/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
@@ -8100,6 +8122,10 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@better-auth/cli/@better-auth/core/better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
"@better-auth/cli/better-auth/better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
"@datadog/datadog-api-client/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
@@ -9468,6 +9494,10 @@
"@aws-sdk/signature-v4/@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@1.1.0", "", { "dependencies": { "@smithy/is-array-buffer": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-9m6NXE0ww+ra5HKHCHig20T+FAwxBAm7DIdwc/767uGWbRcY720ybgPacQNB96JMOI7xVr/CDa3oMzKmW4a+kw=="],
"@better-auth/cli/@better-auth/core/better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
"@better-auth/cli/better-auth/better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
"@dotenvx/dotenvx/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],

View File

@@ -25,6 +25,7 @@ COPY apps/checkout/package.json apps/checkout/
COPY apps/docs/package.json apps/docs/
COPY apps/sdk-test/package.json apps/sdk-test/
COPY apps/website/package.json apps/website/
COPY packages/ai-sdk/package.json packages/ai-sdk/
COPY packages/atmn/package.json packages/atmn/
COPY packages/atmn-tests/package.json packages/atmn-tests/
COPY packages/auth/package.json packages/auth/

View File

@@ -12,6 +12,7 @@
"duplicates"
],
"ignoreWorkspaces": [
"packages/ai-sdk",
"packages/atmn",
"packages/autumn-js",
"packages/mcp",

View File

@@ -122,6 +122,24 @@ actions:
res = autumn.track(customer_id="cus_123", feature_id="messages", value=1)
# Handle response
print(res)
- target: $["paths"]["/v1/balances.track_tokens"]["post"]
update:
x-codeSamples:
- lang: python
label: Python (SDK)
source: |-
from autumn_sdk import Autumn
with Autumn(
x_api_version="2.3.0",
secret_key="<YOUR_BEARER_TOKEN_HERE>",
) as autumn:
res = autumn.track_tokens(customer_id="cus_123", model_id="anthropic/claude-sonnet-4-20250514", input_tokens=1000, output_tokens=500, feature_id="ai_credits")
# Handle response
print(res)
- target: $["paths"]["/v1/balances.update"]["post"]

File diff suppressed because it is too large Load Diff

View File

@@ -200,6 +200,9 @@ Use this to gate access before a feature action. Enable sendEvent when you want
* [track](docs/sdks/autumn/README.md#track) - Records usage for a customer feature and returns updated balances.
Use this after an action happens to decrement usage, or send a negative value to credit balance back.
* [track_tokens](docs/sdks/autumn/README.md#track_tokens) - Records AI token usage for a customer and returns the updated AI credit balance.
Use this after an LLM request when you have input and output token counts. Autumn converts token usage to a dollar amount using the configured model pricing and markup, then tracks that value against the customer's AI credit system.
* [batch_track](docs/sdks/autumn/README.md#batch_track) - Enqueue up to 1000 usage events for asynchronous processing. Items are validated synchronously up front; validated items are then enqueued via SQS for background deduction by workers. The response returns 202 immediately and does not include balance information. On partial enqueue failure (some items fail to enqueue, others succeed), the endpoint still returns 202 and logs the failures server-side; clients should NOT retry, because retrying re-enqueues the already-succeeded items. A 503 is returned only when zero items were successfully enqueued (queue entirely unavailable) — that case is safe to retry.
### [Balances](docs/sdks/balances/README.md)

View File

@@ -5,7 +5,7 @@ from autumn_sdk import errors, models, utils
from autumn_sdk._hooks import HookContext
from autumn_sdk.types import BaseModel, OptionalNullable, UNSET
from autumn_sdk.utils.unmarshal_json_response import unmarshal_json_response
from typing import List, Mapping, Optional, Union, cast
from typing import Dict, List, Mapping, Optional, Union, cast
class Features(BaseSDK):
@@ -13,21 +13,34 @@ class Features(BaseSDK):
self,
*,
name: str,
type_: models.CreateFeatureTypeRequest,
type_: models.CreateFeatureTypeRequestBody,
feature_id: str,
consumable: Optional[bool] = None,
display: Optional[
Union[
models.CreateFeatureDisplayRequest,
models.CreateFeatureDisplayRequestTypedDict,
models.CreateFeatureDisplayRequestBody,
models.CreateFeatureDisplayRequestBodyTypedDict,
]
] = None,
credit_schema: Optional[
Union[
List[models.CreateFeatureCreditSchemaRequest],
List[models.CreateFeatureCreditSchemaRequestTypedDict],
List[models.CreateFeatureCreditSchemaRequestBody],
List[models.CreateFeatureCreditSchemaRequestBodyTypedDict],
]
] = None,
model_markups: OptionalNullable[
Union[
Dict[str, models.CreateFeatureModelMarkupsRequest],
Dict[str, models.CreateFeatureModelMarkupsRequestTypedDict],
]
] = UNSET,
default_markup: Optional[float] = None,
provider_markups: OptionalNullable[
Union[
Dict[str, models.CreateFeatureProviderMarkupsRequest],
Dict[str, models.CreateFeatureProviderMarkupsRequestTypedDict],
]
] = UNSET,
event_names: Optional[List[str]] = None,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
@@ -43,7 +56,10 @@ class Features(BaseSDK):
:param feature_id: The ID of the feature to create.
:param consumable: Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features.
:param display: Singular and plural display names for the feature in your user interface.
:param credit_schema: A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features.
:param credit_schema: A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead.
:param model_markups: Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration.
:param default_markup: Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free.
:param provider_markups: Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id.
:param event_names:
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
@@ -65,10 +81,20 @@ class Features(BaseSDK):
type=type_,
consumable=consumable,
display=utils.get_pydantic_model(
display, Optional[models.CreateFeatureDisplayRequest]
display, Optional[models.CreateFeatureDisplayRequestBody]
),
credit_schema=utils.get_pydantic_model(
credit_schema, Optional[List[models.CreateFeatureCreditSchemaRequest]]
credit_schema,
Optional[List[models.CreateFeatureCreditSchemaRequestBody]],
),
model_markups=utils.get_pydantic_model(
model_markups,
OptionalNullable[Dict[str, models.CreateFeatureModelMarkupsRequest]],
),
default_markup=default_markup,
provider_markups=utils.get_pydantic_model(
provider_markups,
OptionalNullable[Dict[str, models.CreateFeatureProviderMarkupsRequest]],
),
event_names=event_names,
feature_id=feature_id,
@@ -137,21 +163,34 @@ class Features(BaseSDK):
self,
*,
name: str,
type_: models.CreateFeatureTypeRequest,
type_: models.CreateFeatureTypeRequestBody,
feature_id: str,
consumable: Optional[bool] = None,
display: Optional[
Union[
models.CreateFeatureDisplayRequest,
models.CreateFeatureDisplayRequestTypedDict,
models.CreateFeatureDisplayRequestBody,
models.CreateFeatureDisplayRequestBodyTypedDict,
]
] = None,
credit_schema: Optional[
Union[
List[models.CreateFeatureCreditSchemaRequest],
List[models.CreateFeatureCreditSchemaRequestTypedDict],
List[models.CreateFeatureCreditSchemaRequestBody],
List[models.CreateFeatureCreditSchemaRequestBodyTypedDict],
]
] = None,
model_markups: OptionalNullable[
Union[
Dict[str, models.CreateFeatureModelMarkupsRequest],
Dict[str, models.CreateFeatureModelMarkupsRequestTypedDict],
]
] = UNSET,
default_markup: Optional[float] = None,
provider_markups: OptionalNullable[
Union[
Dict[str, models.CreateFeatureProviderMarkupsRequest],
Dict[str, models.CreateFeatureProviderMarkupsRequestTypedDict],
]
] = UNSET,
event_names: Optional[List[str]] = None,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
@@ -167,7 +206,10 @@ class Features(BaseSDK):
:param feature_id: The ID of the feature to create.
:param consumable: Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features.
:param display: Singular and plural display names for the feature in your user interface.
:param credit_schema: A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features.
:param credit_schema: A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead.
:param model_markups: Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration.
:param default_markup: Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free.
:param provider_markups: Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id.
:param event_names:
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
@@ -189,10 +231,20 @@ class Features(BaseSDK):
type=type_,
consumable=consumable,
display=utils.get_pydantic_model(
display, Optional[models.CreateFeatureDisplayRequest]
display, Optional[models.CreateFeatureDisplayRequestBody]
),
credit_schema=utils.get_pydantic_model(
credit_schema, Optional[List[models.CreateFeatureCreditSchemaRequest]]
credit_schema,
Optional[List[models.CreateFeatureCreditSchemaRequestBody]],
),
model_markups=utils.get_pydantic_model(
model_markups,
OptionalNullable[Dict[str, models.CreateFeatureModelMarkupsRequest]],
),
default_markup=default_markup,
provider_markups=utils.get_pydantic_model(
provider_markups,
OptionalNullable[Dict[str, models.CreateFeatureProviderMarkupsRequest]],
),
event_names=event_names,
feature_id=feature_id,
@@ -628,20 +680,33 @@ class Features(BaseSDK):
*,
feature_id: str,
name: Optional[str] = None,
type_: Optional[models.UpdateFeatureTypeRequest] = None,
type_: Optional[models.UpdateFeatureTypeRequestBody] = None,
consumable: Optional[bool] = None,
display: Optional[
Union[
models.UpdateFeatureDisplayRequest,
models.UpdateFeatureDisplayRequestTypedDict,
models.UpdateFeatureDisplayRequestBody,
models.UpdateFeatureDisplayRequestBodyTypedDict,
]
] = None,
credit_schema: Optional[
Union[
List[models.UpdateFeatureCreditSchemaRequest],
List[models.UpdateFeatureCreditSchemaRequestTypedDict],
List[models.UpdateFeatureCreditSchemaRequestBody],
List[models.UpdateFeatureCreditSchemaRequestBodyTypedDict],
]
] = None,
model_markups: OptionalNullable[
Union[
Dict[str, models.UpdateFeatureModelMarkupsRequest],
Dict[str, models.UpdateFeatureModelMarkupsRequestTypedDict],
]
] = UNSET,
default_markup: Optional[float] = None,
provider_markups: OptionalNullable[
Union[
Dict[str, models.UpdateFeatureProviderMarkupsRequest],
Dict[str, models.UpdateFeatureProviderMarkupsRequestTypedDict],
]
] = UNSET,
event_names: Optional[List[str]] = None,
archived: Optional[bool] = None,
new_feature_id: Optional[str] = None,
@@ -659,7 +724,10 @@ class Features(BaseSDK):
:param type: The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system.
:param consumable: Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features.
:param display: Singular and plural display names for the feature in your user interface.
:param credit_schema: A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features.
:param credit_schema: A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead.
:param model_markups: Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration.
:param default_markup: Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free.
:param provider_markups: Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id.
:param event_names:
:param archived: Whether the feature is archived. Archived features are hidden from the dashboard.
:param new_feature_id: The new ID of the feature. Feature ID can only be updated if it's not being used by any customers.
@@ -683,10 +751,20 @@ class Features(BaseSDK):
type=type_,
consumable=consumable,
display=utils.get_pydantic_model(
display, Optional[models.UpdateFeatureDisplayRequest]
display, Optional[models.UpdateFeatureDisplayRequestBody]
),
credit_schema=utils.get_pydantic_model(
credit_schema, Optional[List[models.UpdateFeatureCreditSchemaRequest]]
credit_schema,
Optional[List[models.UpdateFeatureCreditSchemaRequestBody]],
),
model_markups=utils.get_pydantic_model(
model_markups,
OptionalNullable[Dict[str, models.UpdateFeatureModelMarkupsRequest]],
),
default_markup=default_markup,
provider_markups=utils.get_pydantic_model(
provider_markups,
OptionalNullable[Dict[str, models.UpdateFeatureProviderMarkupsRequest]],
),
event_names=event_names,
archived=archived,
@@ -758,20 +836,33 @@ class Features(BaseSDK):
*,
feature_id: str,
name: Optional[str] = None,
type_: Optional[models.UpdateFeatureTypeRequest] = None,
type_: Optional[models.UpdateFeatureTypeRequestBody] = None,
consumable: Optional[bool] = None,
display: Optional[
Union[
models.UpdateFeatureDisplayRequest,
models.UpdateFeatureDisplayRequestTypedDict,
models.UpdateFeatureDisplayRequestBody,
models.UpdateFeatureDisplayRequestBodyTypedDict,
]
] = None,
credit_schema: Optional[
Union[
List[models.UpdateFeatureCreditSchemaRequest],
List[models.UpdateFeatureCreditSchemaRequestTypedDict],
List[models.UpdateFeatureCreditSchemaRequestBody],
List[models.UpdateFeatureCreditSchemaRequestBodyTypedDict],
]
] = None,
model_markups: OptionalNullable[
Union[
Dict[str, models.UpdateFeatureModelMarkupsRequest],
Dict[str, models.UpdateFeatureModelMarkupsRequestTypedDict],
]
] = UNSET,
default_markup: Optional[float] = None,
provider_markups: OptionalNullable[
Union[
Dict[str, models.UpdateFeatureProviderMarkupsRequest],
Dict[str, models.UpdateFeatureProviderMarkupsRequestTypedDict],
]
] = UNSET,
event_names: Optional[List[str]] = None,
archived: Optional[bool] = None,
new_feature_id: Optional[str] = None,
@@ -789,7 +880,10 @@ class Features(BaseSDK):
:param type: The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system.
:param consumable: Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features.
:param display: Singular and plural display names for the feature in your user interface.
:param credit_schema: A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features.
:param credit_schema: A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead.
:param model_markups: Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration.
:param default_markup: Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free.
:param provider_markups: Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id.
:param event_names:
:param archived: Whether the feature is archived. Archived features are hidden from the dashboard.
:param new_feature_id: The new ID of the feature. Feature ID can only be updated if it's not being used by any customers.
@@ -813,10 +907,20 @@ class Features(BaseSDK):
type=type_,
consumable=consumable,
display=utils.get_pydantic_model(
display, Optional[models.UpdateFeatureDisplayRequest]
display, Optional[models.UpdateFeatureDisplayRequestBody]
),
credit_schema=utils.get_pydantic_model(
credit_schema, Optional[List[models.UpdateFeatureCreditSchemaRequest]]
credit_schema,
Optional[List[models.UpdateFeatureCreditSchemaRequestBody]],
),
model_markups=utils.get_pydantic_model(
model_markups,
OptionalNullable[Dict[str, models.UpdateFeatureModelMarkupsRequest]],
),
default_markup=default_markup,
provider_markups=utils.get_pydantic_model(
provider_markups,
OptionalNullable[Dict[str, models.UpdateFeatureProviderMarkupsRequest]],
),
event_names=event_names,
archived=archived,

File diff suppressed because it is too large Load Diff

View File

@@ -261,8 +261,8 @@ class AttachItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class AttachItemPrice(BaseModel):
@@ -288,8 +288,8 @@ class AttachItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -303,15 +303,24 @@ class AttachItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -594,8 +603,8 @@ class AttachAddItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class AttachAddItemPrice(BaseModel):
@@ -621,8 +630,8 @@ class AttachAddItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -636,15 +645,24 @@ class AttachAddItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -809,7 +827,20 @@ AttachRemoveItemBillingMethod = Literal[
r"""Match items with this billing method (prepaid or usage_based)."""
AttachRemoveItemInterval = Literal[
AttachIntervalRemoveItemEnum2 = Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
AttachIntervalRemoveItemEnum1 = Literal[
"one_off",
"week",
"month",
@@ -817,7 +848,20 @@ AttachRemoveItemInterval = Literal[
"semi_annual",
"year",
]
r"""Match items with this interval."""
AttachIntervalUnionTypedDict = TypeAliasType(
"AttachIntervalUnionTypedDict",
Union[AttachIntervalRemoveItemEnum1, AttachIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
AttachIntervalUnion = TypeAliasType(
"AttachIntervalUnion",
Union[AttachIntervalRemoveItemEnum1, AttachIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
class AttachPlanItemFilterTypedDict(TypedDict):
@@ -827,8 +871,10 @@ class AttachPlanItemFilterTypedDict(TypedDict):
r"""Match items linked to this feature."""
billing_method: NotRequired[AttachRemoveItemBillingMethod]
r"""Match items with this billing method (prepaid or usage_based)."""
interval: NotRequired[AttachRemoveItemInterval]
r"""Match items with this interval."""
interval: NotRequired[AttachIntervalUnionTypedDict]
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: NotRequired[int]
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
class AttachPlanItemFilter(BaseModel):
@@ -840,12 +886,17 @@ class AttachPlanItemFilter(BaseModel):
billing_method: Optional[AttachRemoveItemBillingMethod] = None
r"""Match items with this billing method (prepaid or usage_based)."""
interval: Optional[AttachRemoveItemInterval] = None
r"""Match items with this interval."""
interval: Optional[AttachIntervalUnion] = None
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: Optional[int] = None
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["feature_id", "billing_method", "interval"])
optional_fields = set(
["feature_id", "billing_method", "interval", "interval_count"]
)
serialized = handler(self)
m = {}
@@ -926,7 +977,7 @@ class AttachCustomizeTypedDict(TypedDict):
price: NotRequired[Nullable[AttachBasePriceTypedDict]]
r"""Override the base price of the plan. Pass null to remove the base price."""
items: NotRequired[List[AttachItemPlanItemTypedDict]]
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: NotRequired[List[AttachAddItemPlanItemTypedDict]]
r"""Items to add to the plan."""
remove_items: NotRequired[List[AttachPlanItemFilterTypedDict]]
@@ -942,7 +993,7 @@ class AttachCustomize(BaseModel):
r"""Override the base price of the plan. Pass null to remove the base price."""
items: Optional[List[AttachItemPlanItem]] = None
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: Optional[List[AttachAddItemPlanItem]] = None
r"""Items to add to the plan."""

View File

@@ -10,7 +10,7 @@ from autumn_sdk.types import (
UnrecognizedStr,
)
from pydantic import model_serializer
from typing import Any, List, Literal, Optional, Union
from typing import Dict, List, Literal, Optional, Union
from typing_extensions import NotRequired, TypeAliasType, TypedDict
@@ -19,10 +19,11 @@ BalanceType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class BalanceCreditSchemaTypedDict(TypedDict):
@@ -40,6 +41,44 @@ class BalanceCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class BalanceModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class BalanceModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class BalanceProviderMarkupsTypedDict(TypedDict):
markup: float
class BalanceProviderMarkups(BaseModel):
markup: float
class BalanceDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -92,7 +131,7 @@ class BalanceFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: BalanceType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -101,6 +140,12 @@ class BalanceFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[BalanceCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, BalanceModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[Nullable[Dict[str, BalanceProviderMarkupsTypedDict]]]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[BalanceDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -115,7 +160,7 @@ class BalanceFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: BalanceType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -129,21 +174,48 @@ class BalanceFeature(BaseModel):
credit_schema: Optional[List[BalanceCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, BalanceModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, BalanceProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[BalanceDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -222,6 +294,42 @@ class BalanceReset(BaseModel):
return m
BalanceToTypedDict = TypeAliasType("BalanceToTypedDict", Union[float, str])
BalanceTo = TypeAliasType("BalanceTo", Union[float, str])
class BalanceTierTypedDict(TypedDict):
to: BalanceToTypedDict
amount: float
flat_amount: NotRequired[float]
class BalanceTier(BaseModel):
to: BalanceTo
amount: float
flat_amount: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["flat_amount"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
BalanceTierBehavior = Union[
Literal[
"graduated",
@@ -251,7 +359,7 @@ class BalancePriceTypedDict(TypedDict):
r"""Maximum quantity that can be purchased, or null for unlimited."""
amount: NotRequired[float]
r"""The per-unit price amount."""
tiers: NotRequired[List[Nullable[Any]]]
tiers: NotRequired[List[BalanceTierTypedDict]]
r"""Tiered pricing configuration if applicable."""
tier_behavior: NotRequired[BalanceTierBehavior]
r"""How tiers are applied: graduated (split across bands) or volume (flat rate for the matched tier)."""
@@ -270,7 +378,7 @@ class BalancePrice(BaseModel):
amount: Optional[float] = None
r"""The per-unit price amount."""
tiers: Optional[List[Nullable[Any]]] = None
tiers: Optional[List[BalanceTier]] = None
r"""Tiered pricing configuration if applicable."""
tier_behavior: Optional[BalanceTierBehavior] = None

View File

@@ -263,8 +263,8 @@ class BillingUpdateItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class BillingUpdateItemPrice(BaseModel):
@@ -290,8 +290,8 @@ class BillingUpdateItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -305,15 +305,24 @@ class BillingUpdateItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -598,8 +607,8 @@ class BillingUpdateAddItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class BillingUpdateAddItemPrice(BaseModel):
@@ -625,8 +634,8 @@ class BillingUpdateAddItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -640,15 +649,24 @@ class BillingUpdateAddItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -813,7 +831,20 @@ BillingUpdateRemoveItemBillingMethod = Literal[
r"""Match items with this billing method (prepaid or usage_based)."""
BillingUpdateRemoveItemInterval = Literal[
BillingUpdateIntervalRemoveItemEnum2 = Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
BillingUpdateIntervalRemoveItemEnum1 = Literal[
"one_off",
"week",
"month",
@@ -821,7 +852,20 @@ BillingUpdateRemoveItemInterval = Literal[
"semi_annual",
"year",
]
r"""Match items with this interval."""
BillingUpdateIntervalUnionTypedDict = TypeAliasType(
"BillingUpdateIntervalUnionTypedDict",
Union[BillingUpdateIntervalRemoveItemEnum1, BillingUpdateIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
BillingUpdateIntervalUnion = TypeAliasType(
"BillingUpdateIntervalUnion",
Union[BillingUpdateIntervalRemoveItemEnum1, BillingUpdateIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
class BillingUpdatePlanItemFilterTypedDict(TypedDict):
@@ -831,8 +875,10 @@ class BillingUpdatePlanItemFilterTypedDict(TypedDict):
r"""Match items linked to this feature."""
billing_method: NotRequired[BillingUpdateRemoveItemBillingMethod]
r"""Match items with this billing method (prepaid or usage_based)."""
interval: NotRequired[BillingUpdateRemoveItemInterval]
r"""Match items with this interval."""
interval: NotRequired[BillingUpdateIntervalUnionTypedDict]
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: NotRequired[int]
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
class BillingUpdatePlanItemFilter(BaseModel):
@@ -844,12 +890,17 @@ class BillingUpdatePlanItemFilter(BaseModel):
billing_method: Optional[BillingUpdateRemoveItemBillingMethod] = None
r"""Match items with this billing method (prepaid or usage_based)."""
interval: Optional[BillingUpdateRemoveItemInterval] = None
r"""Match items with this interval."""
interval: Optional[BillingUpdateIntervalUnion] = None
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: Optional[int] = None
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["feature_id", "billing_method", "interval"])
optional_fields = set(
["feature_id", "billing_method", "interval", "interval_count"]
)
serialized = handler(self)
m = {}
@@ -930,7 +981,7 @@ class BillingUpdateCustomizeTypedDict(TypedDict):
price: NotRequired[Nullable[BillingUpdateBasePriceTypedDict]]
r"""Override the base price of the plan. Pass null to remove the base price."""
items: NotRequired[List[BillingUpdateItemPlanItemTypedDict]]
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: NotRequired[List[BillingUpdateAddItemPlanItemTypedDict]]
r"""Items to add to the plan."""
remove_items: NotRequired[List[BillingUpdatePlanItemFilterTypedDict]]
@@ -946,7 +997,7 @@ class BillingUpdateCustomize(BaseModel):
r"""Override the base price of the plan. Pass null to remove the base price."""
items: Optional[List[BillingUpdateItemPlanItem]] = None
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: Optional[List[BillingUpdateAddItemPlanItem]] = None
r"""Items to add to the plan."""

View File

@@ -164,10 +164,11 @@ FlagType2 = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class CheckCreditSchema2TypedDict(TypedDict):
@@ -185,6 +186,44 @@ class CheckCreditSchema2(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class CheckModelMarkups2TypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class CheckModelMarkups2(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class CheckProviderMarkups2TypedDict(TypedDict):
markup: float
class CheckProviderMarkups2(BaseModel):
markup: float
class FlagDisplay2TypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -237,7 +276,7 @@ class CheckFeature2TypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: FlagType2
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -246,6 +285,12 @@ class CheckFeature2TypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[CheckCreditSchema2TypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, CheckModelMarkups2TypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[Nullable[Dict[str, CheckProviderMarkups2TypedDict]]]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[FlagDisplay2TypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -260,7 +305,7 @@ class CheckFeature2(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: FlagType2
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -274,21 +319,48 @@ class CheckFeature2(BaseModel):
credit_schema: Optional[List[CheckCreditSchema2]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, CheckModelMarkups2]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, CheckProviderMarkups2]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[FlagDisplay2] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -1106,10 +1178,11 @@ FlagType1 = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class CheckCreditSchema1TypedDict(TypedDict):
@@ -1127,6 +1200,44 @@ class CheckCreditSchema1(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class CheckModelMarkups1TypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class CheckModelMarkups1(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class CheckProviderMarkups1TypedDict(TypedDict):
markup: float
class CheckProviderMarkups1(BaseModel):
markup: float
class FlagDisplay1TypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -1179,7 +1290,7 @@ class CheckFeature1TypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: FlagType1
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -1188,6 +1299,12 @@ class CheckFeature1TypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[CheckCreditSchema1TypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, CheckModelMarkups1TypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[Nullable[Dict[str, CheckProviderMarkups1TypedDict]]]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[FlagDisplay1TypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -1202,7 +1319,7 @@ class CheckFeature1(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: FlagType1
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -1216,21 +1333,48 @@ class CheckFeature1(BaseModel):
credit_schema: Optional[List[CheckCreditSchema1]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, CheckModelMarkups1]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, CheckProviderMarkups1]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[FlagDisplay1] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -483,10 +483,11 @@ CreateEntityType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class CreateEntityCreditSchemaTypedDict(TypedDict):
@@ -504,6 +505,44 @@ class CreateEntityCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class CreateEntityModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class CreateEntityModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class CreateEntityProviderMarkupsTypedDict(TypedDict):
markup: float
class CreateEntityProviderMarkups(BaseModel):
markup: float
class CreateEntityDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -556,7 +595,7 @@ class CreateEntityFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: CreateEntityType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -565,6 +604,14 @@ class CreateEntityFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[CreateEntityCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, CreateEntityModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, CreateEntityProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[CreateEntityDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -579,7 +626,7 @@ class CreateEntityFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: CreateEntityType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -593,21 +640,48 @@ class CreateEntityFeature(BaseModel):
credit_schema: Optional[List[CreateEntityCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, CreateEntityModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, CreateEntityProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[CreateEntityDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -12,7 +12,7 @@ from autumn_sdk.types import (
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import List, Literal, Optional, Union
from typing import Dict, List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypedDict
@@ -44,22 +44,23 @@ class CreateFeatureGlobals(BaseModel):
return m
CreateFeatureTypeRequest = Literal[
CreateFeatureTypeRequestBody = Literal[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
]
r"""The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system."""
class CreateFeatureDisplayRequestTypedDict(TypedDict):
class CreateFeatureDisplayRequestBodyTypedDict(TypedDict):
r"""Singular and plural display names for the feature in your user interface."""
singular: str
plural: str
class CreateFeatureDisplayRequest(BaseModel):
class CreateFeatureDisplayRequestBody(BaseModel):
r"""Singular and plural display names for the feature in your user interface."""
singular: str
@@ -67,57 +68,33 @@ class CreateFeatureDisplayRequest(BaseModel):
plural: str
class CreateFeatureCreditSchemaRequestTypedDict(TypedDict):
class CreateFeatureCreditSchemaRequestBodyTypedDict(TypedDict):
metered_feature_id: str
credit_cost: float
class CreateFeatureCreditSchemaRequest(BaseModel):
class CreateFeatureCreditSchemaRequestBody(BaseModel):
metered_feature_id: str
credit_cost: float
class CreateFeatureParamsTypedDict(TypedDict):
name: str
r"""The name of the feature."""
type: CreateFeatureTypeRequest
r"""The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system."""
feature_id: str
r"""The ID of the feature to create."""
consumable: NotRequired[bool]
r"""Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features."""
display: NotRequired[CreateFeatureDisplayRequestTypedDict]
r"""Singular and plural display names for the feature in your user interface."""
credit_schema: NotRequired[List[CreateFeatureCreditSchemaRequestTypedDict]]
r"""A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features."""
event_names: NotRequired[List[str]]
class CreateFeatureModelMarkupsRequestTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class CreateFeatureParams(BaseModel):
name: str
r"""The name of the feature."""
class CreateFeatureModelMarkupsRequest(BaseModel):
markup: Optional[float] = None
type: CreateFeatureTypeRequest
r"""The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system."""
input_cost: Optional[float] = None
feature_id: str
r"""The ID of the feature to create."""
consumable: Optional[bool] = None
r"""Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features."""
display: Optional[CreateFeatureDisplayRequest] = None
r"""Singular and plural display names for the feature in your user interface."""
credit_schema: Optional[List[CreateFeatureCreditSchemaRequest]] = None
r"""A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features."""
event_names: Optional[List[str]] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["consumable", "display", "credit_schema", "event_names"])
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
@@ -132,15 +109,118 @@ class CreateFeatureParams(BaseModel):
return m
class CreateFeatureProviderMarkupsRequestTypedDict(TypedDict):
markup: float
class CreateFeatureProviderMarkupsRequest(BaseModel):
markup: float
class CreateFeatureParamsTypedDict(TypedDict):
name: str
r"""The name of the feature."""
type: CreateFeatureTypeRequestBody
r"""The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system."""
feature_id: str
r"""The ID of the feature to create."""
consumable: NotRequired[bool]
r"""Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features."""
display: NotRequired[CreateFeatureDisplayRequestBodyTypedDict]
r"""Singular and plural display names for the feature in your user interface."""
credit_schema: NotRequired[List[CreateFeatureCreditSchemaRequestBodyTypedDict]]
r"""A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead."""
model_markups: NotRequired[
Nullable[Dict[str, CreateFeatureModelMarkupsRequestTypedDict]]
]
r"""Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration."""
default_markup: NotRequired[float]
r"""Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, CreateFeatureProviderMarkupsRequestTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id."""
event_names: NotRequired[List[str]]
class CreateFeatureParams(BaseModel):
name: str
r"""The name of the feature."""
type: CreateFeatureTypeRequestBody
r"""The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system."""
feature_id: str
r"""The ID of the feature to create."""
consumable: Optional[bool] = None
r"""Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features."""
display: Optional[CreateFeatureDisplayRequestBody] = None
r"""Singular and plural display names for the feature in your user interface."""
credit_schema: Optional[List[CreateFeatureCreditSchemaRequestBody]] = None
r"""A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead."""
model_markups: OptionalNullable[Dict[str, CreateFeatureModelMarkupsRequest]] = UNSET
r"""Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration."""
default_markup: Optional[float] = None
r"""Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free."""
provider_markups: OptionalNullable[
Dict[str, CreateFeatureProviderMarkupsRequest]
] = UNSET
r"""Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id."""
event_names: Optional[List[str]] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(
[
"consumable",
"display",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"event_names",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
CreateFeatureTypeResponse = Union[
Literal[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class CreateFeatureCreditSchemaResponseTypedDict(TypedDict):
@@ -158,6 +238,44 @@ class CreateFeatureCreditSchemaResponse(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class CreateFeatureModelMarkupsResponseTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class CreateFeatureModelMarkupsResponse(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class CreateFeatureProviderMarkupsResponseTypedDict(TypedDict):
markup: float
class CreateFeatureProviderMarkupsResponse(BaseModel):
markup: float
class CreateFeatureDisplayResponseTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -210,7 +328,7 @@ class CreateFeatureResponseTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: CreateFeatureTypeResponse
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -219,6 +337,16 @@ class CreateFeatureResponseTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[CreateFeatureCreditSchemaResponseTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[
Nullable[Dict[str, CreateFeatureModelMarkupsResponseTypedDict]]
]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, CreateFeatureProviderMarkupsResponseTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[CreateFeatureDisplayResponseTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -233,7 +361,7 @@ class CreateFeatureResponse(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: CreateFeatureTypeResponse
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -247,21 +375,52 @@ class CreateFeatureResponse(BaseModel):
credit_schema: Optional[List[CreateFeatureCreditSchemaResponse]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, CreateFeatureModelMarkupsResponse]] = (
UNSET
)
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[
Dict[str, CreateFeatureProviderMarkupsResponse]
] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[CreateFeatureDisplayResponse] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -12,7 +12,7 @@ from autumn_sdk.types import (
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import Any, List, Literal, Optional, Union
from typing import List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict
@@ -44,7 +44,7 @@ class CreatePlanGlobals(BaseModel):
return m
CreatePlanPriceIntervalRequest = Literal[
CreatePlanPriceIntervalRequestBody = Literal[
"one_off",
"week",
"month",
@@ -55,24 +55,24 @@ CreatePlanPriceIntervalRequest = Literal[
r"""Billing interval (e.g. 'month', 'year')."""
class CreatePlanPriceRequestTypedDict(TypedDict):
class CreatePlanPriceRequestBodyTypedDict(TypedDict):
r"""Base recurring price for the plan. Omit for free or usage-only plans."""
amount: float
r"""Base price amount for the plan."""
interval: CreatePlanPriceIntervalRequest
interval: CreatePlanPriceIntervalRequestBody
r"""Billing interval (e.g. 'month', 'year')."""
interval_count: NotRequired[float]
r"""Number of intervals per billing cycle. Defaults to 1."""
class CreatePlanPriceRequest(BaseModel):
class CreatePlanPriceRequestBody(BaseModel):
r"""Base recurring price for the plan. Omit for free or usage-only plans."""
amount: float
r"""Base price amount for the plan."""
interval: CreatePlanPriceIntervalRequest
interval: CreatePlanPriceIntervalRequestBody
r"""Billing interval (e.g. 'month', 'year')."""
interval_count: Optional[float] = None
@@ -95,7 +95,7 @@ class CreatePlanPriceRequest(BaseModel):
return m
CreatePlanResetIntervalRequest = Literal[
CreatePlanResetIntervalRequestBody = Literal[
"one_off",
"minute",
"hour",
@@ -109,19 +109,19 @@ CreatePlanResetIntervalRequest = Literal[
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
class CreatePlanResetRequestTypedDict(TypedDict):
class CreatePlanResetRequestBodyTypedDict(TypedDict):
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
interval: CreatePlanResetIntervalRequest
interval: CreatePlanResetIntervalRequestBody
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
interval_count: NotRequired[float]
r"""Number of intervals between resets. Defaults to 1."""
class CreatePlanResetRequest(BaseModel):
class CreatePlanResetRequestBody(BaseModel):
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
interval: CreatePlanResetIntervalRequest
interval: CreatePlanResetIntervalRequestBody
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
interval_count: Optional[float] = None
@@ -144,20 +144,22 @@ class CreatePlanResetRequest(BaseModel):
return m
CreatePlanToTypedDict = TypeAliasType("CreatePlanToTypedDict", Union[float, str])
CreatePlanToRequestBodyTypedDict = TypeAliasType(
"CreatePlanToRequestBodyTypedDict", Union[float, str]
)
CreatePlanTo = TypeAliasType("CreatePlanTo", Union[float, str])
CreatePlanToRequestBody = TypeAliasType("CreatePlanToRequestBody", Union[float, str])
class CreatePlanTierTypedDict(TypedDict):
to: CreatePlanToTypedDict
class CreatePlanTierRequestBodyTypedDict(TypedDict):
to: CreatePlanToRequestBodyTypedDict
amount: NotRequired[float]
flat_amount: NotRequired[float]
class CreatePlanTier(BaseModel):
to: CreatePlanTo
class CreatePlanTierRequestBody(BaseModel):
to: CreatePlanToRequestBody
amount: Optional[float] = None
@@ -180,13 +182,13 @@ class CreatePlanTier(BaseModel):
return m
CreatePlanTierBehaviorRequest = Literal[
CreatePlanTierBehaviorRequestBody = Literal[
"graduated",
"volume",
]
CreatePlanItemPriceIntervalRequest = Literal[
CreatePlanItemPriceIntervalRequestBody = Literal[
"one_off",
"week",
"month",
@@ -197,49 +199,49 @@ CreatePlanItemPriceIntervalRequest = Literal[
r"""Billing interval. For consumable features, should match reset.interval."""
CreatePlanBillingMethodRequest = Literal[
CreatePlanBillingMethodRequestBody = Literal[
"prepaid",
"usage_based",
]
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
class CreatePlanItemPriceRequestTypedDict(TypedDict):
class CreatePlanItemPriceRequestBodyTypedDict(TypedDict):
r"""Pricing for usage beyond included units. Omit for free features."""
interval: CreatePlanItemPriceIntervalRequest
interval: CreatePlanItemPriceIntervalRequestBody
r"""Billing interval. For consumable features, should match reset.interval."""
billing_method: CreatePlanBillingMethodRequest
billing_method: CreatePlanBillingMethodRequestBody
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
amount: NotRequired[float]
r"""Price per billing_units after included usage. Either 'amount' or 'tiers' is required."""
tiers: NotRequired[List[CreatePlanTierTypedDict]]
tiers: NotRequired[List[CreatePlanTierRequestBodyTypedDict]]
r"""Tiered pricing. Either 'amount' or 'tiers' is required."""
tier_behavior: NotRequired[CreatePlanTierBehaviorRequest]
tier_behavior: NotRequired[CreatePlanTierBehaviorRequestBody]
interval_count: NotRequired[float]
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class CreatePlanItemPriceRequest(BaseModel):
class CreatePlanItemPriceRequestBody(BaseModel):
r"""Pricing for usage beyond included units. Omit for free features."""
interval: CreatePlanItemPriceIntervalRequest
interval: CreatePlanItemPriceIntervalRequestBody
r"""Billing interval. For consumable features, should match reset.interval."""
billing_method: CreatePlanBillingMethodRequest
billing_method: CreatePlanBillingMethodRequestBody
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
amount: Optional[float] = None
r"""Price per billing_units after included usage. Either 'amount' or 'tiers' is required."""
tiers: Optional[List[CreatePlanTier]] = None
tiers: Optional[List[CreatePlanTierRequestBody]] = None
r"""Tiered pricing. Either 'amount' or 'tiers' is required."""
tier_behavior: Optional[CreatePlanTierBehaviorRequest] = None
tier_behavior: Optional[CreatePlanTierBehaviorRequestBody] = None
interval_count: Optional[float] = 1
r"""Number of intervals per billing cycle. Defaults to 1."""
@@ -247,8 +249,8 @@ class CreatePlanItemPriceRequest(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -262,15 +264,24 @@ class CreatePlanItemPriceRequest(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -314,17 +325,17 @@ class CreatePlanProration(BaseModel):
r"""Credit behavior when quantity decreases mid-cycle."""
CreatePlanExpiryDurationTypeRequest = Literal[
CreatePlanExpiryDurationTypeRequestBody = Literal[
"month",
"forever",
]
r"""When rolled over units expire."""
class CreatePlanRolloverRequestTypedDict(TypedDict):
class CreatePlanRolloverRequestBodyTypedDict(TypedDict):
r"""Rollover config for unused units. If set, unused included units carry over."""
expiry_duration_type: CreatePlanExpiryDurationTypeRequest
expiry_duration_type: CreatePlanExpiryDurationTypeRequestBody
r"""When rolled over units expire."""
max: NotRequired[float]
r"""Max rollover units. Omit for unlimited rollover."""
@@ -334,10 +345,10 @@ class CreatePlanRolloverRequestTypedDict(TypedDict):
r"""Number of periods before expiry."""
class CreatePlanRolloverRequest(BaseModel):
class CreatePlanRolloverRequestBody(BaseModel):
r"""Rollover config for unused units. If set, unused included units carry over."""
expiry_duration_type: CreatePlanExpiryDurationTypeRequest
expiry_duration_type: CreatePlanExpiryDurationTypeRequestBody
r"""When rolled over units expire."""
max: Optional[float] = None
@@ -375,13 +386,13 @@ class CreatePlanPlanItemTypedDict(TypedDict):
r"""Number of free units included. Balance resets to this each interval for consumable features."""
unlimited: NotRequired[bool]
r"""If true, customer has unlimited access to this feature."""
reset: NotRequired[CreatePlanResetRequestTypedDict]
reset: NotRequired[CreatePlanResetRequestBodyTypedDict]
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
price: NotRequired[CreatePlanItemPriceRequestTypedDict]
price: NotRequired[CreatePlanItemPriceRequestBodyTypedDict]
r"""Pricing for usage beyond included units. Omit for free features."""
proration: NotRequired[CreatePlanProrationTypedDict]
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
rollover: NotRequired[CreatePlanRolloverRequestTypedDict]
rollover: NotRequired[CreatePlanRolloverRequestBodyTypedDict]
r"""Rollover config for unused units. If set, unused included units carry over."""
@@ -397,16 +408,16 @@ class CreatePlanPlanItem(BaseModel):
unlimited: Optional[bool] = None
r"""If true, customer has unlimited access to this feature."""
reset: Optional[CreatePlanResetRequest] = None
reset: Optional[CreatePlanResetRequestBody] = None
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
price: Optional[CreatePlanItemPriceRequest] = None
price: Optional[CreatePlanItemPriceRequestBody] = None
r"""Pricing for usage beyond included units. Omit for free features."""
proration: Optional[CreatePlanProration] = None
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
rollover: Optional[CreatePlanRolloverRequest] = None
rollover: Optional[CreatePlanRolloverRequestBody] = None
r"""Rollover config for unused units. If set, unused included units carry over."""
@model_serializer(mode="wrap")
@@ -531,7 +542,7 @@ class CreatePlanParamsTypedDict(TypedDict):
r"""If true, this plan can be attached alongside other plans. Otherwise, attaching replaces existing plans in the same group."""
auto_enable: NotRequired[bool]
r"""If true, plan is automatically attached when a customer is created. Use for free tiers."""
price: NotRequired[CreatePlanPriceRequestTypedDict]
price: NotRequired[CreatePlanPriceRequestBodyTypedDict]
r"""Base recurring price for the plan. Omit for free or usage-only plans."""
items: NotRequired[List[CreatePlanPlanItemTypedDict]]
r"""Feature configurations for this plan. Each item defines included units, pricing, and reset behavior."""
@@ -561,7 +572,7 @@ class CreatePlanParams(BaseModel):
auto_enable: Optional[bool] = False
r"""If true, plan is automatically attached when a customer is created. Use for free tiers."""
price: Optional[CreatePlanPriceRequest] = None
price: Optional[CreatePlanPriceRequestBody] = None
r"""Base recurring price for the plan. Omit for free or usage-only plans."""
items: Optional[List[CreatePlanPlanItem]] = None
@@ -710,6 +721,7 @@ CreatePlanType = Union[
"single_use",
"continuous_use",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
@@ -858,6 +870,44 @@ class CreatePlanResetResponse(BaseModel):
return m
CreatePlanToResponseTypedDict = TypeAliasType(
"CreatePlanToResponseTypedDict", Union[float, str]
)
CreatePlanToResponse = TypeAliasType("CreatePlanToResponse", Union[float, str])
class CreatePlanTierResponseTypedDict(TypedDict):
to: CreatePlanToResponseTypedDict
amount: float
flat_amount: NotRequired[float]
class CreatePlanTierResponse(BaseModel):
to: CreatePlanToResponse
amount: float
flat_amount: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["flat_amount"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
CreatePlanTierBehaviorResponse = Union[
Literal[
"graduated",
@@ -902,7 +952,7 @@ class CreatePlanItemPriceResponseTypedDict(TypedDict):
r"""Maximum units a customer can purchase beyond included. E.g. if included=100 and max_purchase=300, customer can use up to 400 total before usage is capped. Null for no limit."""
amount: NotRequired[float]
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: NotRequired[List[Nullable[Any]]]
tiers: NotRequired[List[CreatePlanTierResponseTypedDict]]
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: NotRequired[CreatePlanTierBehaviorResponse]
interval_count: NotRequired[float]
@@ -925,7 +975,7 @@ class CreatePlanItemPriceResponse(BaseModel):
amount: Optional[float] = None
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: Optional[List[Nullable[Any]]] = None
tiers: Optional[List[CreatePlanTierResponse]] = None
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: Optional[CreatePlanTierBehaviorResponse] = None

View File

@@ -14,7 +14,7 @@ import pydantic
from pydantic import model_serializer
from pydantic.functional_validators import AfterValidator
from typing import Any, Dict, List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypedDict
from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict
class CreateScheduleGlobalsTypedDict(TypedDict):
@@ -243,7 +243,7 @@ class CreateScheduleBasePrice2(BaseModel):
return m
CreateScheduleResetInterval2 = Literal[
CreateScheduleItemResetInterval2 = Literal[
"one_off",
"minute",
"hour",
@@ -257,19 +257,19 @@ CreateScheduleResetInterval2 = Literal[
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
class CreateScheduleReset2TypedDict(TypedDict):
class CreateScheduleItemReset2TypedDict(TypedDict):
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
interval: CreateScheduleResetInterval2
interval: CreateScheduleItemResetInterval2
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
interval_count: NotRequired[float]
r"""Number of intervals between resets. Defaults to 1."""
class CreateScheduleReset2(BaseModel):
class CreateScheduleItemReset2(BaseModel):
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
interval: CreateScheduleResetInterval2
interval: CreateScheduleItemResetInterval2
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
interval_count: Optional[float] = None
@@ -292,13 +292,13 @@ class CreateScheduleReset2(BaseModel):
return m
class CreateScheduleTier2TypedDict(TypedDict):
class CreateScheduleItemTier2TypedDict(TypedDict):
to: NotRequired[Any]
amount: NotRequired[Any]
flat_amount: NotRequired[Any]
class CreateScheduleTier2(BaseModel):
class CreateScheduleItemTier2(BaseModel):
to: Optional[Any] = None
amount: Optional[Any] = None
@@ -322,7 +322,7 @@ class CreateScheduleTier2(BaseModel):
return m
CreateScheduleTierBehavior2 = Literal[
CreateScheduleItemTierBehavior2 = Literal[
"graduated",
"volume",
]
@@ -339,49 +339,49 @@ CreateScheduleItemPriceInterval2 = Literal[
r"""Billing interval. For consumable features, should match reset.interval."""
CreateScheduleBillingMethod2 = Literal[
CreateScheduleItemBillingMethod2 = Literal[
"prepaid",
"usage_based",
]
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
class CreateSchedulePrice2TypedDict(TypedDict):
class CreateScheduleItemPrice2TypedDict(TypedDict):
r"""Pricing for usage beyond included units. Omit for free features."""
interval: CreateScheduleItemPriceInterval2
r"""Billing interval. For consumable features, should match reset.interval."""
billing_method: CreateScheduleBillingMethod2
billing_method: CreateScheduleItemBillingMethod2
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
amount: NotRequired[float]
r"""Price per billing_units after included usage. Either 'amount' or 'tiers' is required."""
tiers: NotRequired[List[CreateScheduleTier2TypedDict]]
tiers: NotRequired[List[CreateScheduleItemTier2TypedDict]]
r"""Tiered pricing. Either 'amount' or 'tiers' is required."""
tier_behavior: NotRequired[CreateScheduleTierBehavior2]
tier_behavior: NotRequired[CreateScheduleItemTierBehavior2]
interval_count: NotRequired[float]
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class CreateSchedulePrice2(BaseModel):
class CreateScheduleItemPrice2(BaseModel):
r"""Pricing for usage beyond included units. Omit for free features."""
interval: CreateScheduleItemPriceInterval2
r"""Billing interval. For consumable features, should match reset.interval."""
billing_method: CreateScheduleBillingMethod2
billing_method: CreateScheduleItemBillingMethod2
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
amount: Optional[float] = None
r"""Price per billing_units after included usage. Either 'amount' or 'tiers' is required."""
tiers: Optional[List[CreateScheduleTier2]] = None
tiers: Optional[List[CreateScheduleItemTier2]] = None
r"""Tiered pricing. Either 'amount' or 'tiers' is required."""
tier_behavior: Optional[CreateScheduleTierBehavior2] = None
tier_behavior: Optional[CreateScheduleItemTierBehavior2] = None
interval_count: Optional[float] = 1
r"""Number of intervals per billing cycle. Defaults to 1."""
@@ -389,8 +389,8 @@ class CreateSchedulePrice2(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -404,21 +404,30 @@ class CreateSchedulePrice2(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
CreateScheduleOnIncrease2 = Literal[
CreateScheduleItemOnIncrease2 = Literal[
"bill_immediately",
"prorate_immediately",
"prorate_next_cycle",
@@ -427,7 +436,7 @@ CreateScheduleOnIncrease2 = Literal[
r"""Billing behavior when quantity increases mid-cycle."""
CreateScheduleOnDecrease2 = Literal[
CreateScheduleItemOnDecrease2 = Literal[
"prorate",
"prorate_immediately",
"prorate_next_cycle",
@@ -437,36 +446,36 @@ CreateScheduleOnDecrease2 = Literal[
r"""Credit behavior when quantity decreases mid-cycle."""
class CreateScheduleProration2TypedDict(TypedDict):
class CreateScheduleItemProration2TypedDict(TypedDict):
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
on_increase: CreateScheduleOnIncrease2
on_increase: CreateScheduleItemOnIncrease2
r"""Billing behavior when quantity increases mid-cycle."""
on_decrease: CreateScheduleOnDecrease2
on_decrease: CreateScheduleItemOnDecrease2
r"""Credit behavior when quantity decreases mid-cycle."""
class CreateScheduleProration2(BaseModel):
class CreateScheduleItemProration2(BaseModel):
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
on_increase: CreateScheduleOnIncrease2
on_increase: CreateScheduleItemOnIncrease2
r"""Billing behavior when quantity increases mid-cycle."""
on_decrease: CreateScheduleOnDecrease2
on_decrease: CreateScheduleItemOnDecrease2
r"""Credit behavior when quantity decreases mid-cycle."""
CreateScheduleExpiryDurationType2 = Literal[
CreateScheduleItemExpiryDurationType2 = Literal[
"month",
"forever",
]
r"""When rolled over units expire."""
class CreateScheduleRollover2TypedDict(TypedDict):
class CreateScheduleItemRollover2TypedDict(TypedDict):
r"""Rollover config for unused units. If set, unused included units carry over."""
expiry_duration_type: CreateScheduleExpiryDurationType2
expiry_duration_type: CreateScheduleItemExpiryDurationType2
r"""When rolled over units expire."""
max: NotRequired[float]
r"""Max rollover units. Omit for unlimited rollover."""
@@ -476,10 +485,10 @@ class CreateScheduleRollover2TypedDict(TypedDict):
r"""Number of periods before expiry."""
class CreateScheduleRollover2(BaseModel):
class CreateScheduleItemRollover2(BaseModel):
r"""Rollover config for unused units. If set, unused included units carry over."""
expiry_duration_type: CreateScheduleExpiryDurationType2
expiry_duration_type: CreateScheduleItemExpiryDurationType2
r"""When rolled over units expire."""
max: Optional[float] = None
@@ -508,7 +517,7 @@ class CreateScheduleRollover2(BaseModel):
return m
class CreateSchedulePlanItem2TypedDict(TypedDict):
class CreateScheduleItemPlanItem2TypedDict(TypedDict):
r"""Configuration for a feature item in a plan, including usage limits, pricing, and rollover settings."""
feature_id: str
@@ -517,17 +526,17 @@ class CreateSchedulePlanItem2TypedDict(TypedDict):
r"""Number of free units included. Balance resets to this each interval for consumable features."""
unlimited: NotRequired[bool]
r"""If true, customer has unlimited access to this feature."""
reset: NotRequired[CreateScheduleReset2TypedDict]
reset: NotRequired[CreateScheduleItemReset2TypedDict]
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
price: NotRequired[CreateSchedulePrice2TypedDict]
price: NotRequired[CreateScheduleItemPrice2TypedDict]
r"""Pricing for usage beyond included units. Omit for free features."""
proration: NotRequired[CreateScheduleProration2TypedDict]
proration: NotRequired[CreateScheduleItemProration2TypedDict]
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
rollover: NotRequired[CreateScheduleRollover2TypedDict]
rollover: NotRequired[CreateScheduleItemRollover2TypedDict]
r"""Rollover config for unused units. If set, unused included units carry over."""
class CreateSchedulePlanItem2(BaseModel):
class CreateScheduleItemPlanItem2(BaseModel):
r"""Configuration for a feature item in a plan, including usage limits, pricing, and rollover settings."""
feature_id: str
@@ -539,16 +548,16 @@ class CreateSchedulePlanItem2(BaseModel):
unlimited: Optional[bool] = None
r"""If true, customer has unlimited access to this feature."""
reset: Optional[CreateScheduleReset2] = None
reset: Optional[CreateScheduleItemReset2] = None
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
price: Optional[CreateSchedulePrice2] = None
price: Optional[CreateScheduleItemPrice2] = None
r"""Pricing for usage beyond included units. Omit for free features."""
proration: Optional[CreateScheduleProration2] = None
proration: Optional[CreateScheduleItemProration2] = None
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
rollover: Optional[CreateScheduleRollover2] = None
rollover: Optional[CreateScheduleItemRollover2] = None
r"""Rollover config for unused units. If set, unused included units carry over."""
@model_serializer(mode="wrap")
@@ -570,27 +579,464 @@ class CreateSchedulePlanItem2(BaseModel):
return m
CreateScheduleAddItemResetInterval2 = Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
class CreateScheduleAddItemReset2TypedDict(TypedDict):
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
interval: CreateScheduleAddItemResetInterval2
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
interval_count: NotRequired[float]
r"""Number of intervals between resets. Defaults to 1."""
class CreateScheduleAddItemReset2(BaseModel):
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
interval: CreateScheduleAddItemResetInterval2
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
interval_count: Optional[float] = None
r"""Number of intervals between resets. Defaults to 1."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["interval_count"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class CreateScheduleAddItemTier2TypedDict(TypedDict):
to: NotRequired[Any]
amount: NotRequired[Any]
flat_amount: NotRequired[Any]
class CreateScheduleAddItemTier2(BaseModel):
to: Optional[Any] = None
amount: Optional[Any] = None
flat_amount: Optional[Any] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["to", "amount", "flat_amount"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
CreateScheduleAddItemTierBehavior2 = Literal[
"graduated",
"volume",
]
CreateScheduleAddItemPriceInterval2 = Literal[
"one_off",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
r"""Billing interval. For consumable features, should match reset.interval."""
CreateScheduleAddItemBillingMethod2 = Literal[
"prepaid",
"usage_based",
]
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
class CreateScheduleAddItemPrice2TypedDict(TypedDict):
r"""Pricing for usage beyond included units. Omit for free features."""
interval: CreateScheduleAddItemPriceInterval2
r"""Billing interval. For consumable features, should match reset.interval."""
billing_method: CreateScheduleAddItemBillingMethod2
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
amount: NotRequired[float]
r"""Price per billing_units after included usage. Either 'amount' or 'tiers' is required."""
tiers: NotRequired[List[CreateScheduleAddItemTier2TypedDict]]
r"""Tiered pricing. Either 'amount' or 'tiers' is required."""
tier_behavior: NotRequired[CreateScheduleAddItemTierBehavior2]
interval_count: NotRequired[float]
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class CreateScheduleAddItemPrice2(BaseModel):
r"""Pricing for usage beyond included units. Omit for free features."""
interval: CreateScheduleAddItemPriceInterval2
r"""Billing interval. For consumable features, should match reset.interval."""
billing_method: CreateScheduleAddItemBillingMethod2
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
amount: Optional[float] = None
r"""Price per billing_units after included usage. Either 'amount' or 'tiers' is required."""
tiers: Optional[List[CreateScheduleAddItemTier2]] = None
r"""Tiered pricing. Either 'amount' or 'tiers' is required."""
tier_behavior: Optional[CreateScheduleAddItemTierBehavior2] = None
interval_count: Optional[float] = 1
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(
[
"amount",
"tiers",
"tier_behavior",
"interval_count",
"billing_units",
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
CreateScheduleAddItemOnIncrease2 = Literal[
"bill_immediately",
"prorate_immediately",
"prorate_next_cycle",
"bill_next_cycle",
]
r"""Billing behavior when quantity increases mid-cycle."""
CreateScheduleAddItemOnDecrease2 = Literal[
"prorate",
"prorate_immediately",
"prorate_next_cycle",
"none",
"no_prorations",
]
r"""Credit behavior when quantity decreases mid-cycle."""
class CreateScheduleAddItemProration2TypedDict(TypedDict):
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
on_increase: CreateScheduleAddItemOnIncrease2
r"""Billing behavior when quantity increases mid-cycle."""
on_decrease: CreateScheduleAddItemOnDecrease2
r"""Credit behavior when quantity decreases mid-cycle."""
class CreateScheduleAddItemProration2(BaseModel):
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
on_increase: CreateScheduleAddItemOnIncrease2
r"""Billing behavior when quantity increases mid-cycle."""
on_decrease: CreateScheduleAddItemOnDecrease2
r"""Credit behavior when quantity decreases mid-cycle."""
CreateScheduleAddItemExpiryDurationType2 = Literal[
"month",
"forever",
]
r"""When rolled over units expire."""
class CreateScheduleAddItemRollover2TypedDict(TypedDict):
r"""Rollover config for unused units. If set, unused included units carry over."""
expiry_duration_type: CreateScheduleAddItemExpiryDurationType2
r"""When rolled over units expire."""
max: NotRequired[float]
r"""Max rollover units. Omit for unlimited rollover."""
max_percentage: NotRequired[float]
r"""Maximum rollover as a percentage (0-100) of included + prepaid grant. Mutually exclusive with max."""
expiry_duration_length: NotRequired[float]
r"""Number of periods before expiry."""
class CreateScheduleAddItemRollover2(BaseModel):
r"""Rollover config for unused units. If set, unused included units carry over."""
expiry_duration_type: CreateScheduleAddItemExpiryDurationType2
r"""When rolled over units expire."""
max: Optional[float] = None
r"""Max rollover units. Omit for unlimited rollover."""
max_percentage: Optional[float] = None
r"""Maximum rollover as a percentage (0-100) of included + prepaid grant. Mutually exclusive with max."""
expiry_duration_length: Optional[float] = None
r"""Number of periods before expiry."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["max", "max_percentage", "expiry_duration_length"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class CreateScheduleAddItemPlanItem2TypedDict(TypedDict):
r"""Configuration for a feature item in a plan, including usage limits, pricing, and rollover settings."""
feature_id: str
r"""The ID of the feature to configure."""
included: NotRequired[float]
r"""Number of free units included. Balance resets to this each interval for consumable features."""
unlimited: NotRequired[bool]
r"""If true, customer has unlimited access to this feature."""
reset: NotRequired[CreateScheduleAddItemReset2TypedDict]
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
price: NotRequired[CreateScheduleAddItemPrice2TypedDict]
r"""Pricing for usage beyond included units. Omit for free features."""
proration: NotRequired[CreateScheduleAddItemProration2TypedDict]
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
rollover: NotRequired[CreateScheduleAddItemRollover2TypedDict]
r"""Rollover config for unused units. If set, unused included units carry over."""
class CreateScheduleAddItemPlanItem2(BaseModel):
r"""Configuration for a feature item in a plan, including usage limits, pricing, and rollover settings."""
feature_id: str
r"""The ID of the feature to configure."""
included: Optional[float] = None
r"""Number of free units included. Balance resets to this each interval for consumable features."""
unlimited: Optional[bool] = None
r"""If true, customer has unlimited access to this feature."""
reset: Optional[CreateScheduleAddItemReset2] = None
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
price: Optional[CreateScheduleAddItemPrice2] = None
r"""Pricing for usage beyond included units. Omit for free features."""
proration: Optional[CreateScheduleAddItemProration2] = None
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
rollover: Optional[CreateScheduleAddItemRollover2] = None
r"""Rollover config for unused units. If set, unused included units carry over."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(
["included", "unlimited", "reset", "price", "proration", "rollover"]
)
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
CreateScheduleRemoveItemBillingMethod2 = Literal[
"prepaid",
"usage_based",
]
r"""Match items with this billing method (prepaid or usage_based)."""
CreateScheduleIntervalRemoveItemEnum4 = Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
CreateScheduleIntervalRemoveItemEnum3 = Literal[
"one_off",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
CreateScheduleIntervalUnion2TypedDict = TypeAliasType(
"CreateScheduleIntervalUnion2TypedDict",
Union[CreateScheduleIntervalRemoveItemEnum3, CreateScheduleIntervalRemoveItemEnum4],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
CreateScheduleIntervalUnion2 = TypeAliasType(
"CreateScheduleIntervalUnion2",
Union[CreateScheduleIntervalRemoveItemEnum3, CreateScheduleIntervalRemoveItemEnum4],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
class CreateSchedulePlanItemFilter2TypedDict(TypedDict):
r"""Filter for matching plan items. All provided fields must match (AND)."""
feature_id: NotRequired[str]
r"""Match items linked to this feature."""
billing_method: NotRequired[CreateScheduleRemoveItemBillingMethod2]
r"""Match items with this billing method (prepaid or usage_based)."""
interval: NotRequired[CreateScheduleIntervalUnion2TypedDict]
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: NotRequired[int]
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
class CreateSchedulePlanItemFilter2(BaseModel):
r"""Filter for matching plan items. All provided fields must match (AND)."""
feature_id: Optional[str] = None
r"""Match items linked to this feature."""
billing_method: Optional[CreateScheduleRemoveItemBillingMethod2] = None
r"""Match items with this billing method (prepaid or usage_based)."""
interval: Optional[CreateScheduleIntervalUnion2] = None
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: Optional[int] = None
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(
["feature_id", "billing_method", "interval", "interval_count"]
)
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class CreateScheduleCustomize2TypedDict(TypedDict):
r"""Customize the plan to schedule. Can override the price, items, or both."""
r"""Customize the plan to schedule. Can override price, replace items, or patch items with add_items, remove_items, and update_items."""
price: NotRequired[Nullable[CreateScheduleBasePrice2TypedDict]]
r"""Override the base price of the plan. Pass null to remove the base price."""
items: NotRequired[List[CreateSchedulePlanItem2TypedDict]]
r"""Override the items in the plan."""
items: NotRequired[List[CreateScheduleItemPlanItem2TypedDict]]
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: NotRequired[List[CreateScheduleAddItemPlanItem2TypedDict]]
r"""Items to add to the plan."""
remove_items: NotRequired[List[CreateSchedulePlanItemFilter2TypedDict]]
r"""Filters selecting items to remove from the plan."""
class CreateScheduleCustomize2(BaseModel):
r"""Customize the plan to schedule. Can override the price, items, or both."""
r"""Customize the plan to schedule. Can override price, replace items, or patch items with add_items, remove_items, and update_items."""
price: OptionalNullable[CreateScheduleBasePrice2] = UNSET
r"""Override the base price of the plan. Pass null to remove the base price."""
items: Optional[List[CreateSchedulePlanItem2]] = None
r"""Override the items in the plan."""
items: Optional[List[CreateScheduleItemPlanItem2]] = None
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: Optional[List[CreateScheduleAddItemPlanItem2]] = None
r"""Items to add to the plan."""
remove_items: Optional[List[CreateSchedulePlanItemFilter2]] = None
r"""Filters selecting items to remove from the plan."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["price", "items"])
optional_fields = set(["price", "items", "add_items", "remove_items"])
nullable_fields = set(["price"])
serialized = handler(self)
m = {}
@@ -622,7 +1068,7 @@ class CreateSchedulePlan2TypedDict(TypedDict):
version: NotRequired[float]
r"""Optional explicit plan version to schedule."""
customize: NotRequired[CreateScheduleCustomize2TypedDict]
r"""Customize the plan to schedule. Can override the price, items, or both."""
r"""Customize the plan to schedule. Can override price, replace items, or patch items with add_items, remove_items, and update_items."""
subscription_id: NotRequired[str]
r"""A unique ID to identify this subscription. Useful when scheduling the same plan multiple times."""
@@ -638,7 +1084,7 @@ class CreateSchedulePlan2(BaseModel):
r"""Optional explicit plan version to schedule."""
customize: Optional[CreateScheduleCustomize2] = None
r"""Customize the plan to schedule. Can override the price, items, or both."""
r"""Customize the plan to schedule. Can override price, replace items, or patch items with add_items, remove_items, and update_items."""
subscription_id: Optional[str] = None
r"""A unique ID to identify this subscription. Useful when scheduling the same plan multiple times."""

View File

@@ -569,10 +569,11 @@ CustomerFlagsType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class CustomerCreditSchemaTypedDict(TypedDict):
@@ -590,6 +591,44 @@ class CustomerCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class CustomerModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class CustomerModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class CustomerProviderMarkupsTypedDict(TypedDict):
markup: float
class CustomerProviderMarkups(BaseModel):
markup: float
class CustomerDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -642,7 +681,7 @@ class CustomerFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: CustomerFlagsType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -651,6 +690,12 @@ class CustomerFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[CustomerCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, CustomerModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[Nullable[Dict[str, CustomerProviderMarkupsTypedDict]]]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[CustomerDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -665,7 +710,7 @@ class CustomerFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: CustomerFlagsType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -679,21 +724,48 @@ class CustomerFeature(BaseModel):
credit_schema: Optional[List[CustomerCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, CustomerModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, CustomerProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[CustomerDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -632,10 +632,11 @@ GetCustomerFlagsType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class GetCustomerCreditSchemaTypedDict(TypedDict):
@@ -653,6 +654,44 @@ class GetCustomerCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class GetCustomerModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class GetCustomerModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class GetCustomerProviderMarkupsTypedDict(TypedDict):
markup: float
class GetCustomerProviderMarkups(BaseModel):
markup: float
class GetCustomerDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -705,7 +744,7 @@ class GetCustomerFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: GetCustomerFlagsType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -714,6 +753,14 @@ class GetCustomerFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[GetCustomerCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, GetCustomerModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, GetCustomerProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[GetCustomerDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -728,7 +775,7 @@ class GetCustomerFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: GetCustomerFlagsType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -742,21 +789,48 @@ class GetCustomerFeature(BaseModel):
credit_schema: Optional[List[GetCustomerCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, GetCustomerModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, GetCustomerProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[GetCustomerDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -291,10 +291,11 @@ GetEntityType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class GetEntityCreditSchemaTypedDict(TypedDict):
@@ -312,6 +313,44 @@ class GetEntityCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class GetEntityModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class GetEntityModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class GetEntityProviderMarkupsTypedDict(TypedDict):
markup: float
class GetEntityProviderMarkups(BaseModel):
markup: float
class GetEntityDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -364,7 +403,7 @@ class GetEntityFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: GetEntityType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -373,6 +412,14 @@ class GetEntityFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[GetEntityCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, GetEntityModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, GetEntityProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[GetEntityDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -387,7 +434,7 @@ class GetEntityFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: GetEntityType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -401,21 +448,48 @@ class GetEntityFeature(BaseModel):
credit_schema: Optional[List[GetEntityCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, GetEntityModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, GetEntityProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[GetEntityDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -12,7 +12,7 @@ from autumn_sdk.types import (
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import List, Literal, Optional, Union
from typing import Dict, List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypedDict
@@ -59,10 +59,11 @@ GetFeatureType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class GetFeatureCreditSchemaTypedDict(TypedDict):
@@ -80,6 +81,44 @@ class GetFeatureCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class GetFeatureModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class GetFeatureModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class GetFeatureProviderMarkupsTypedDict(TypedDict):
markup: float
class GetFeatureProviderMarkups(BaseModel):
markup: float
class GetFeatureDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -132,7 +171,7 @@ class GetFeatureResponseTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: GetFeatureType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -141,6 +180,14 @@ class GetFeatureResponseTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[GetFeatureCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, GetFeatureModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, GetFeatureProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[GetFeatureDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -155,7 +202,7 @@ class GetFeatureResponse(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: GetFeatureType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -169,21 +216,48 @@ class GetFeatureResponse(BaseModel):
credit_schema: Optional[List[GetFeatureCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, GetFeatureModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, GetFeatureProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[GetFeatureDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -12,8 +12,8 @@ from autumn_sdk.types import (
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import Any, List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypedDict
from typing import List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict
class GetPlanGlobalsTypedDict(TypedDict):
@@ -172,6 +172,7 @@ GetPlanType = Union[
"single_use",
"continuous_use",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
@@ -320,6 +321,42 @@ class GetPlanReset(BaseModel):
return m
GetPlanToTypedDict = TypeAliasType("GetPlanToTypedDict", Union[float, str])
GetPlanTo = TypeAliasType("GetPlanTo", Union[float, str])
class GetPlanTierTypedDict(TypedDict):
to: GetPlanToTypedDict
amount: float
flat_amount: NotRequired[float]
class GetPlanTier(BaseModel):
to: GetPlanTo
amount: float
flat_amount: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["flat_amount"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
GetPlanTierBehavior = Union[
Literal[
"graduated",
@@ -364,7 +401,7 @@ class GetPlanItemPriceTypedDict(TypedDict):
r"""Maximum units a customer can purchase beyond included. E.g. if included=100 and max_purchase=300, customer can use up to 400 total before usage is capped. Null for no limit."""
amount: NotRequired[float]
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: NotRequired[List[Nullable[Any]]]
tiers: NotRequired[List[GetPlanTierTypedDict]]
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: NotRequired[GetPlanTierBehavior]
interval_count: NotRequired[float]
@@ -387,7 +424,7 @@ class GetPlanItemPrice(BaseModel):
amount: Optional[float] = None
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: Optional[List[Nullable[Any]]] = None
tiers: Optional[List[GetPlanTier]] = None
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: Optional[GetPlanTierBehavior] = None

View File

@@ -701,10 +701,11 @@ ListCustomersType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class ListCustomersCreditSchemaTypedDict(TypedDict):
@@ -722,6 +723,44 @@ class ListCustomersCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class ListCustomersModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class ListCustomersModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class ListCustomersProviderMarkupsTypedDict(TypedDict):
markup: float
class ListCustomersProviderMarkups(BaseModel):
markup: float
class ListCustomersDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -774,7 +813,7 @@ class ListCustomersFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: ListCustomersType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -783,6 +822,14 @@ class ListCustomersFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[ListCustomersCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, ListCustomersModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, ListCustomersProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[ListCustomersDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -797,7 +844,7 @@ class ListCustomersFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: ListCustomersType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -811,21 +858,48 @@ class ListCustomersFeature(BaseModel):
credit_schema: Optional[List[ListCustomersCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, ListCustomersModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, ListCustomersProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[ListCustomersDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -367,10 +367,11 @@ ListEntitiesType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class ListEntitiesCreditSchemaTypedDict(TypedDict):
@@ -388,6 +389,44 @@ class ListEntitiesCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class ListEntitiesModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class ListEntitiesModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class ListEntitiesProviderMarkupsTypedDict(TypedDict):
markup: float
class ListEntitiesProviderMarkups(BaseModel):
markup: float
class ListEntitiesDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -440,7 +479,7 @@ class ListEntitiesFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: ListEntitiesType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -449,6 +488,14 @@ class ListEntitiesFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[ListEntitiesCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, ListEntitiesModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, ListEntitiesProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[ListEntitiesDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -463,7 +510,7 @@ class ListEntitiesFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: ListEntitiesType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -477,21 +524,48 @@ class ListEntitiesFeature(BaseModel):
credit_schema: Optional[List[ListEntitiesCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, ListEntitiesModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, ListEntitiesProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[ListEntitiesDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -12,7 +12,7 @@ from autumn_sdk.types import (
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import List, Literal, Optional, Union
from typing import Dict, List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypedDict
@@ -57,10 +57,11 @@ ListFeaturesType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class ListFeaturesCreditSchemaTypedDict(TypedDict):
@@ -78,6 +79,44 @@ class ListFeaturesCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class ListFeaturesModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class ListFeaturesModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class ListFeaturesProviderMarkupsTypedDict(TypedDict):
markup: float
class ListFeaturesProviderMarkups(BaseModel):
markup: float
class ListFeaturesDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -128,7 +167,7 @@ class ListFeaturesListTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: ListFeaturesType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -137,6 +176,14 @@ class ListFeaturesListTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[ListFeaturesCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, ListFeaturesModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, ListFeaturesProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[ListFeaturesDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -149,7 +196,7 @@ class ListFeaturesList(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: ListFeaturesType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -163,21 +210,48 @@ class ListFeaturesList(BaseModel):
credit_schema: Optional[List[ListFeaturesCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, ListFeaturesModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, ListFeaturesProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[ListFeaturesDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -12,8 +12,8 @@ from autumn_sdk.types import (
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import Any, List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypedDict
from typing import List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict
class ListPlansGlobalsTypedDict(TypedDict):
@@ -177,6 +177,7 @@ ListPlansType = Union[
"single_use",
"continuous_use",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
@@ -325,6 +326,42 @@ class ListPlansReset(BaseModel):
return m
ListPlansToTypedDict = TypeAliasType("ListPlansToTypedDict", Union[float, str])
ListPlansTo = TypeAliasType("ListPlansTo", Union[float, str])
class ListPlansTierTypedDict(TypedDict):
to: ListPlansToTypedDict
amount: float
flat_amount: NotRequired[float]
class ListPlansTier(BaseModel):
to: ListPlansTo
amount: float
flat_amount: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["flat_amount"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
ListPlansTierBehavior = Union[
Literal[
"graduated",
@@ -369,7 +406,7 @@ class ListPlansItemPriceTypedDict(TypedDict):
r"""Maximum units a customer can purchase beyond included. E.g. if included=100 and max_purchase=300, customer can use up to 400 total before usage is capped. Null for no limit."""
amount: NotRequired[float]
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: NotRequired[List[Nullable[Any]]]
tiers: NotRequired[List[ListPlansTierTypedDict]]
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: NotRequired[ListPlansTierBehavior]
interval_count: NotRequired[float]
@@ -392,7 +429,7 @@ class ListPlansItemPrice(BaseModel):
amount: Optional[float] = None
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: Optional[List[Nullable[Any]]] = None
tiers: Optional[List[ListPlansTier]] = None
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: Optional[ListPlansTierBehavior] = None

View File

@@ -221,8 +221,8 @@ class MultiAttachPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class MultiAttachPrice(BaseModel):
@@ -248,8 +248,8 @@ class MultiAttachPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -263,15 +263,24 @@ class MultiAttachPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -10,8 +10,8 @@ from autumn_sdk.types import (
UnrecognizedStr,
)
from pydantic import model_serializer
from typing import Any, List, Literal, Optional, Union
from typing_extensions import NotRequired, TypedDict
from typing import List, Literal, Optional, Union
from typing_extensions import NotRequired, TypeAliasType, TypedDict
PlanPriceInterval = Union[
@@ -111,6 +111,7 @@ PlanType = Union[
"single_use",
"continuous_use",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
@@ -259,6 +260,42 @@ class PlanReset(BaseModel):
return m
PlanToTypedDict = TypeAliasType("PlanToTypedDict", Union[float, str])
PlanTo = TypeAliasType("PlanTo", Union[float, str])
class PlanTierTypedDict(TypedDict):
to: PlanToTypedDict
amount: float
flat_amount: NotRequired[float]
class PlanTier(BaseModel):
to: PlanTo
amount: float
flat_amount: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["flat_amount"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
PlanTierBehavior = Union[
Literal[
"graduated",
@@ -303,7 +340,7 @@ class PlanItemPriceTypedDict(TypedDict):
r"""Maximum units a customer can purchase beyond included. E.g. if included=100 and max_purchase=300, customer can use up to 400 total before usage is capped. Null for no limit."""
amount: NotRequired[float]
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: NotRequired[List[Nullable[Any]]]
tiers: NotRequired[List[PlanTierTypedDict]]
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: NotRequired[PlanTierBehavior]
interval_count: NotRequired[float]
@@ -326,7 +363,7 @@ class PlanItemPrice(BaseModel):
amount: Optional[float] = None
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: Optional[List[Nullable[Any]]] = None
tiers: Optional[List[PlanTier]] = None
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: Optional[PlanTierBehavior] = None

View File

@@ -264,8 +264,8 @@ class PreviewAttachItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class PreviewAttachItemPrice(BaseModel):
@@ -291,8 +291,8 @@ class PreviewAttachItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -306,15 +306,24 @@ class PreviewAttachItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -599,8 +608,8 @@ class PreviewAttachAddItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class PreviewAttachAddItemPrice(BaseModel):
@@ -626,8 +635,8 @@ class PreviewAttachAddItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -641,15 +650,24 @@ class PreviewAttachAddItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -814,7 +832,20 @@ PreviewAttachRemoveItemBillingMethod = Literal[
r"""Match items with this billing method (prepaid or usage_based)."""
PreviewAttachRemoveItemInterval = Literal[
PreviewAttachIntervalRemoveItemEnum2 = Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
PreviewAttachIntervalRemoveItemEnum1 = Literal[
"one_off",
"week",
"month",
@@ -822,7 +853,20 @@ PreviewAttachRemoveItemInterval = Literal[
"semi_annual",
"year",
]
r"""Match items with this interval."""
PreviewAttachIntervalUnionTypedDict = TypeAliasType(
"PreviewAttachIntervalUnionTypedDict",
Union[PreviewAttachIntervalRemoveItemEnum1, PreviewAttachIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
PreviewAttachIntervalUnion = TypeAliasType(
"PreviewAttachIntervalUnion",
Union[PreviewAttachIntervalRemoveItemEnum1, PreviewAttachIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
class PreviewAttachPlanItemFilterTypedDict(TypedDict):
@@ -832,8 +876,10 @@ class PreviewAttachPlanItemFilterTypedDict(TypedDict):
r"""Match items linked to this feature."""
billing_method: NotRequired[PreviewAttachRemoveItemBillingMethod]
r"""Match items with this billing method (prepaid or usage_based)."""
interval: NotRequired[PreviewAttachRemoveItemInterval]
r"""Match items with this interval."""
interval: NotRequired[PreviewAttachIntervalUnionTypedDict]
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: NotRequired[int]
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
class PreviewAttachPlanItemFilter(BaseModel):
@@ -845,12 +891,17 @@ class PreviewAttachPlanItemFilter(BaseModel):
billing_method: Optional[PreviewAttachRemoveItemBillingMethod] = None
r"""Match items with this billing method (prepaid or usage_based)."""
interval: Optional[PreviewAttachRemoveItemInterval] = None
r"""Match items with this interval."""
interval: Optional[PreviewAttachIntervalUnion] = None
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: Optional[int] = None
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["feature_id", "billing_method", "interval"])
optional_fields = set(
["feature_id", "billing_method", "interval", "interval_count"]
)
serialized = handler(self)
m = {}
@@ -931,7 +982,7 @@ class PreviewAttachCustomizeTypedDict(TypedDict):
price: NotRequired[Nullable[PreviewAttachBasePriceTypedDict]]
r"""Override the base price of the plan. Pass null to remove the base price."""
items: NotRequired[List[PreviewAttachItemPlanItemTypedDict]]
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: NotRequired[List[PreviewAttachAddItemPlanItemTypedDict]]
r"""Items to add to the plan."""
remove_items: NotRequired[List[PreviewAttachPlanItemFilterTypedDict]]
@@ -947,7 +998,7 @@ class PreviewAttachCustomize(BaseModel):
r"""Override the base price of the plan. Pass null to remove the base price."""
items: Optional[List[PreviewAttachItemPlanItem]] = None
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: Optional[List[PreviewAttachAddItemPlanItem]] = None
r"""Items to add to the plan."""

View File

@@ -224,8 +224,8 @@ class PreviewMultiAttachPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class PreviewMultiAttachPrice(BaseModel):
@@ -251,8 +251,8 @@ class PreviewMultiAttachPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -266,15 +266,24 @@ class PreviewMultiAttachPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -264,8 +264,8 @@ class PreviewUpdateItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class PreviewUpdateItemPrice(BaseModel):
@@ -291,8 +291,8 @@ class PreviewUpdateItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -306,15 +306,24 @@ class PreviewUpdateItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -599,8 +608,8 @@ class PreviewUpdateAddItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class PreviewUpdateAddItemPrice(BaseModel):
@@ -626,8 +635,8 @@ class PreviewUpdateAddItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -641,15 +650,24 @@ class PreviewUpdateAddItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -814,7 +832,20 @@ PreviewUpdateRemoveItemBillingMethod = Literal[
r"""Match items with this billing method (prepaid or usage_based)."""
PreviewUpdateRemoveItemInterval = Literal[
PreviewUpdateIntervalRemoveItemEnum2 = Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
PreviewUpdateIntervalRemoveItemEnum1 = Literal[
"one_off",
"week",
"month",
@@ -822,7 +853,20 @@ PreviewUpdateRemoveItemInterval = Literal[
"semi_annual",
"year",
]
r"""Match items with this interval."""
PreviewUpdateIntervalUnionTypedDict = TypeAliasType(
"PreviewUpdateIntervalUnionTypedDict",
Union[PreviewUpdateIntervalRemoveItemEnum1, PreviewUpdateIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
PreviewUpdateIntervalUnion = TypeAliasType(
"PreviewUpdateIntervalUnion",
Union[PreviewUpdateIntervalRemoveItemEnum1, PreviewUpdateIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
class PreviewUpdatePlanItemFilterTypedDict(TypedDict):
@@ -832,8 +876,10 @@ class PreviewUpdatePlanItemFilterTypedDict(TypedDict):
r"""Match items linked to this feature."""
billing_method: NotRequired[PreviewUpdateRemoveItemBillingMethod]
r"""Match items with this billing method (prepaid or usage_based)."""
interval: NotRequired[PreviewUpdateRemoveItemInterval]
r"""Match items with this interval."""
interval: NotRequired[PreviewUpdateIntervalUnionTypedDict]
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: NotRequired[int]
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
class PreviewUpdatePlanItemFilter(BaseModel):
@@ -845,12 +891,17 @@ class PreviewUpdatePlanItemFilter(BaseModel):
billing_method: Optional[PreviewUpdateRemoveItemBillingMethod] = None
r"""Match items with this billing method (prepaid or usage_based)."""
interval: Optional[PreviewUpdateRemoveItemInterval] = None
r"""Match items with this interval."""
interval: Optional[PreviewUpdateIntervalUnion] = None
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: Optional[int] = None
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["feature_id", "billing_method", "interval"])
optional_fields = set(
["feature_id", "billing_method", "interval", "interval_count"]
)
serialized = handler(self)
m = {}
@@ -931,7 +982,7 @@ class PreviewUpdateCustomizeTypedDict(TypedDict):
price: NotRequired[Nullable[PreviewUpdateBasePriceTypedDict]]
r"""Override the base price of the plan. Pass null to remove the base price."""
items: NotRequired[List[PreviewUpdateItemPlanItemTypedDict]]
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: NotRequired[List[PreviewUpdateAddItemPlanItemTypedDict]]
r"""Items to add to the plan."""
remove_items: NotRequired[List[PreviewUpdatePlanItemFilterTypedDict]]
@@ -947,7 +998,7 @@ class PreviewUpdateCustomize(BaseModel):
r"""Override the base price of the plan. Pass null to remove the base price."""
items: Optional[List[PreviewUpdateItemPlanItem]] = None
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: Optional[List[PreviewUpdateAddItemPlanItem]] = None
r"""Items to add to the plan."""

View File

@@ -262,8 +262,8 @@ class SetupPaymentItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class SetupPaymentItemPrice(BaseModel):
@@ -289,8 +289,8 @@ class SetupPaymentItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -304,15 +304,24 @@ class SetupPaymentItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -597,8 +606,8 @@ class SetupPaymentAddItemPriceTypedDict(TypedDict):
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class SetupPaymentAddItemPrice(BaseModel):
@@ -624,8 +633,8 @@ class SetupPaymentAddItemPrice(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -639,15 +648,24 @@ class SetupPaymentAddItemPrice(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -812,7 +830,20 @@ SetupPaymentRemoveItemBillingMethod = Literal[
r"""Match items with this billing method (prepaid or usage_based)."""
SetupPaymentRemoveItemInterval = Literal[
SetupPaymentIntervalRemoveItemEnum2 = Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
]
SetupPaymentIntervalRemoveItemEnum1 = Literal[
"one_off",
"week",
"month",
@@ -820,7 +851,20 @@ SetupPaymentRemoveItemInterval = Literal[
"semi_annual",
"year",
]
r"""Match items with this interval."""
SetupPaymentIntervalUnionTypedDict = TypeAliasType(
"SetupPaymentIntervalUnionTypedDict",
Union[SetupPaymentIntervalRemoveItemEnum1, SetupPaymentIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
SetupPaymentIntervalUnion = TypeAliasType(
"SetupPaymentIntervalUnion",
Union[SetupPaymentIntervalRemoveItemEnum1, SetupPaymentIntervalRemoveItemEnum2],
)
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
class SetupPaymentPlanItemFilterTypedDict(TypedDict):
@@ -830,8 +874,10 @@ class SetupPaymentPlanItemFilterTypedDict(TypedDict):
r"""Match items linked to this feature."""
billing_method: NotRequired[SetupPaymentRemoveItemBillingMethod]
r"""Match items with this billing method (prepaid or usage_based)."""
interval: NotRequired[SetupPaymentRemoveItemInterval]
r"""Match items with this interval."""
interval: NotRequired[SetupPaymentIntervalUnionTypedDict]
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: NotRequired[int]
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
class SetupPaymentPlanItemFilter(BaseModel):
@@ -843,12 +889,17 @@ class SetupPaymentPlanItemFilter(BaseModel):
billing_method: Optional[SetupPaymentRemoveItemBillingMethod] = None
r"""Match items with this billing method (prepaid or usage_based)."""
interval: Optional[SetupPaymentRemoveItemInterval] = None
r"""Match items with this interval."""
interval: Optional[SetupPaymentIntervalUnion] = None
r"""Match items with this interval. Accepts either a BillingInterval (price-side) or a ResetInterval (reset-side, includes day/hour/minute) so price-less items keyed by reset.interval can be disambiguated."""
interval_count: Optional[int] = None
r"""Match items with this interval_count. Disambiguates between items that share an interval but differ in count."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["feature_id", "billing_method", "interval"])
optional_fields = set(
["feature_id", "billing_method", "interval", "interval_count"]
)
serialized = handler(self)
m = {}
@@ -929,7 +980,7 @@ class SetupPaymentCustomizeTypedDict(TypedDict):
price: NotRequired[Nullable[SetupPaymentBasePriceTypedDict]]
r"""Override the base price of the plan. Pass null to remove the base price."""
items: NotRequired[List[SetupPaymentItemPlanItemTypedDict]]
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: NotRequired[List[SetupPaymentAddItemPlanItemTypedDict]]
r"""Items to add to the plan."""
remove_items: NotRequired[List[SetupPaymentPlanItemFilterTypedDict]]
@@ -945,7 +996,7 @@ class SetupPaymentCustomize(BaseModel):
r"""Override the base price of the plan. Pass null to remove the base price."""
items: Optional[List[SetupPaymentItemPlanItem]] = None
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / update_items."""
r"""Override the items in the plan (PUT-style — replaces all existing items). Mutually exclusive with add_items / remove_items / deprecated update_items."""
add_items: Optional[List[SetupPaymentAddItemPlanItem]] = None
r"""Items to add to the plan."""

View File

@@ -220,7 +220,7 @@ class TrackReset2(BaseModel):
return m
class Deduction2TypedDict(TypedDict):
class TrackDeduction2TypedDict(TypedDict):
balance_id: str
r"""ID of the underlying balance row that was deducted from (customer_entitlement or rollover)."""
feature_id: str
@@ -233,7 +233,7 @@ class Deduction2TypedDict(TypedDict):
r"""Amount deducted from this balance. Positive when usage was consumed, negative when credit was restored (e.g. a refund via negative track value)."""
class Deduction2(BaseModel):
class TrackDeduction2(BaseModel):
balance_id: str
r"""ID of the underlying balance row that was deducted from (customer_entitlement or rollover)."""
@@ -279,7 +279,7 @@ class TrackResponseBody2TypedDict(TypedDict):
r"""The event name that was tracked, if event_name was used instead of feature_id."""
balances: NotRequired[Dict[str, Nullable[BalanceTypedDict]]]
r"""Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature."""
deductions: NotRequired[List[Deduction2TypedDict]]
deductions: NotRequired[List[TrackDeduction2TypedDict]]
r"""Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling."""
@@ -304,7 +304,7 @@ class TrackResponseBody2(BaseModel):
balances: Optional[Dict[str, Nullable[Balance]]] = None
r"""Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature."""
deductions: Optional[List[Deduction2]] = None
deductions: Optional[List[TrackDeduction2]] = None
r"""Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling."""
@model_serializer(mode="wrap")
@@ -406,7 +406,7 @@ class TrackReset1(BaseModel):
return m
class Deduction1TypedDict(TypedDict):
class TrackDeduction1TypedDict(TypedDict):
balance_id: str
r"""ID of the underlying balance row that was deducted from (customer_entitlement or rollover)."""
feature_id: str
@@ -419,7 +419,7 @@ class Deduction1TypedDict(TypedDict):
r"""Amount deducted from this balance. Positive when usage was consumed, negative when credit was restored (e.g. a refund via negative track value)."""
class Deduction1(BaseModel):
class TrackDeduction1(BaseModel):
balance_id: str
r"""ID of the underlying balance row that was deducted from (customer_entitlement or rollover)."""
@@ -465,7 +465,7 @@ class TrackResponseBody1TypedDict(TypedDict):
r"""The event name that was tracked, if event_name was used instead of feature_id."""
balances: NotRequired[Dict[str, Nullable[BalanceTypedDict]]]
r"""Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature."""
deductions: NotRequired[List[Deduction1TypedDict]]
deductions: NotRequired[List[TrackDeduction1TypedDict]]
r"""Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling."""
@@ -490,7 +490,7 @@ class TrackResponseBody1(BaseModel):
balances: Optional[Dict[str, Nullable[Balance]]] = None
r"""Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature."""
deductions: Optional[List[Deduction1]] = None
deductions: Optional[List[TrackDeduction1]] = None
r"""Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling."""
@model_serializer(mode="wrap")

View File

@@ -0,0 +1,513 @@
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
from __future__ import annotations
from .balance import Balance, BalanceTypedDict
from autumn_sdk.types import BaseModel, Nullable, UNSET_SENTINEL, UnrecognizedStr
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import Any, Dict, List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict
class TrackTokensGlobalsTypedDict(TypedDict):
x_api_version: NotRequired[str]
class TrackTokensGlobals(BaseModel):
x_api_version: Annotated[
Optional[str],
pydantic.Field(alias="x-api-version"),
FieldMetadata(header=HeaderMetadata(style="simple", explode=False)),
] = "2.3.0"
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["x-api-version"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class TrackTokensParamsTypedDict(TypedDict):
customer_id: str
r"""The ID of the customer."""
model_id: str
r"""The AI model as '<provider>/<model>' (e.g. 'anthropic/claude-opus-4-8', 'openrouter/openai/gpt-4o'). The provider is the first path segment and must match a provider + model key in models.dev."""
input_tokens: int
r"""Number of non-cached text input tokens consumed. Exclusive of cache and audio token pools."""
output_tokens: int
r"""Number of text output tokens consumed. Exclusive of the reasoning and audio output pools."""
entity_id: NotRequired[str]
r"""The ID of the entity for entity-scoped balances."""
feature_id: NotRequired[str]
r"""The ID of the AI credit system feature. Auto-detected from the customer's entitlements if omitted — only required when a customer has multiple AI credit systems."""
cache_read_tokens: NotRequired[int]
r"""Number of cached input tokens read."""
cache_write_tokens: NotRequired[int]
r"""Number of input tokens written to the cache."""
audio_input_tokens: NotRequired[int]
r"""Number of audio input tokens consumed."""
audio_output_tokens: NotRequired[int]
r"""Number of audio output tokens generated."""
reasoning_tokens: NotRequired[int]
r"""Number of reasoning tokens generated."""
properties: NotRequired[Dict[str, Any]]
r"""Additional properties to attach to this usage event."""
class TrackTokensParams(BaseModel):
customer_id: str
r"""The ID of the customer."""
model_id: str
r"""The AI model as '<provider>/<model>' (e.g. 'anthropic/claude-opus-4-8', 'openrouter/openai/gpt-4o'). The provider is the first path segment and must match a provider + model key in models.dev."""
input_tokens: int
r"""Number of non-cached text input tokens consumed. Exclusive of cache and audio token pools."""
output_tokens: int
r"""Number of text output tokens consumed. Exclusive of the reasoning and audio output pools."""
entity_id: Optional[str] = None
r"""The ID of the entity for entity-scoped balances."""
feature_id: Optional[str] = None
r"""The ID of the AI credit system feature. Auto-detected from the customer's entitlements if omitted — only required when a customer has multiple AI credit systems."""
cache_read_tokens: Optional[int] = None
r"""Number of cached input tokens read."""
cache_write_tokens: Optional[int] = None
r"""Number of input tokens written to the cache."""
audio_input_tokens: Optional[int] = None
r"""Number of audio input tokens consumed."""
audio_output_tokens: Optional[int] = None
r"""Number of audio output tokens generated."""
reasoning_tokens: Optional[int] = None
r"""Number of reasoning tokens generated."""
properties: Optional[Dict[str, Any]] = None
r"""Additional properties to attach to this usage event."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(
[
"entity_id",
"feature_id",
"cache_read_tokens",
"cache_write_tokens",
"audio_input_tokens",
"audio_output_tokens",
"reasoning_tokens",
"properties",
]
)
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
TrackTokensIntervalEnum2 = Union[
Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
],
UnrecognizedStr,
]
TrackTokensIntervalUnion2TypedDict = TypeAliasType(
"TrackTokensIntervalUnion2TypedDict", Union[TrackTokensIntervalEnum2, str]
)
r"""The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals."""
TrackTokensIntervalUnion2 = TypeAliasType(
"TrackTokensIntervalUnion2", Union[TrackTokensIntervalEnum2, str]
)
r"""The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals."""
class TrackTokensReset2TypedDict(TypedDict):
interval: TrackTokensIntervalUnion2TypedDict
r"""The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals."""
resets_at: Nullable[float]
r"""Timestamp when the balance will next reset."""
interval_count: NotRequired[float]
r"""Number of intervals between resets (eg. 2 for bi-monthly)."""
class TrackTokensReset2(BaseModel):
interval: TrackTokensIntervalUnion2
r"""The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals."""
resets_at: Nullable[float]
r"""Timestamp when the balance will next reset."""
interval_count: Optional[float] = None
r"""Number of intervals between resets (eg. 2 for bi-monthly)."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["interval_count"])
nullable_fields = set(["resets_at"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
class TrackTokensDeduction2TypedDict(TypedDict):
balance_id: str
r"""ID of the underlying balance row that was deducted from (customer_entitlement or rollover)."""
feature_id: str
r"""The feature this balance belongs to."""
plan_id: Nullable[str]
r"""ID of the plan/product this balance belongs to. Null when the balance can't be attributed to a single plan (e.g. it spans multiple)."""
reset: Nullable[TrackTokensReset2TypedDict]
r"""Reset configuration for the balance this deduction came from, or null if the balance doesn't reset."""
value: float
r"""Amount deducted from this balance. Positive when usage was consumed, negative when credit was restored (e.g. a refund via negative track value)."""
class TrackTokensDeduction2(BaseModel):
balance_id: str
r"""ID of the underlying balance row that was deducted from (customer_entitlement or rollover)."""
feature_id: str
r"""The feature this balance belongs to."""
plan_id: Nullable[str]
r"""ID of the plan/product this balance belongs to. Null when the balance can't be attributed to a single plan (e.g. it spans multiple)."""
reset: Nullable[TrackTokensReset2]
r"""Reset configuration for the balance this deduction came from, or null if the balance doesn't reset."""
value: float
r"""Amount deducted from this balance. Positive when usage was consumed, negative when credit was restored (e.g. a refund via negative track value)."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
m[k] = val
return m
class TrackTokensResponseBody2TypedDict(TypedDict):
r"""Accepted. Autumn is experiencing degraded service from a downstream provider, so the token usage event was accepted for replay and will be tracked as soon as the service is restored."""
customer_id: str
r"""The ID of the customer whose usage was tracked."""
value: float
r"""The amount of usage that was recorded."""
balance: Nullable[BalanceTypedDict]
r"""The updated balance for the tracked feature. Null if tracking by event_name that affects multiple features."""
entity_id: NotRequired[str]
r"""The ID of the entity, if entity-scoped tracking was performed."""
event_name: NotRequired[str]
r"""The event name that was tracked, if event_name was used instead of feature_id."""
balances: NotRequired[Dict[str, Nullable[BalanceTypedDict]]]
r"""Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature."""
deductions: NotRequired[List[TrackTokensDeduction2TypedDict]]
r"""Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling."""
class TrackTokensResponseBody2(BaseModel):
r"""Accepted. Autumn is experiencing degraded service from a downstream provider, so the token usage event was accepted for replay and will be tracked as soon as the service is restored."""
customer_id: str
r"""The ID of the customer whose usage was tracked."""
value: float
r"""The amount of usage that was recorded."""
balance: Nullable[Balance]
r"""The updated balance for the tracked feature. Null if tracking by event_name that affects multiple features."""
entity_id: Optional[str] = None
r"""The ID of the entity, if entity-scoped tracking was performed."""
event_name: Optional[str] = None
r"""The event name that was tracked, if event_name was used instead of feature_id."""
balances: Optional[Dict[str, Nullable[Balance]]] = None
r"""Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature."""
deductions: Optional[List[TrackTokensDeduction2]] = None
r"""Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["entity_id", "event_name", "balances", "deductions"])
nullable_fields = set(["balance"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
TrackTokensIntervalEnum1 = Union[
Literal[
"one_off",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"semi_annual",
"year",
],
UnrecognizedStr,
]
TrackTokensIntervalUnion1TypedDict = TypeAliasType(
"TrackTokensIntervalUnion1TypedDict", Union[TrackTokensIntervalEnum1, str]
)
r"""The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals."""
TrackTokensIntervalUnion1 = TypeAliasType(
"TrackTokensIntervalUnion1", Union[TrackTokensIntervalEnum1, str]
)
r"""The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals."""
class TrackTokensReset1TypedDict(TypedDict):
interval: TrackTokensIntervalUnion1TypedDict
r"""The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals."""
resets_at: Nullable[float]
r"""Timestamp when the balance will next reset."""
interval_count: NotRequired[float]
r"""Number of intervals between resets (eg. 2 for bi-monthly)."""
class TrackTokensReset1(BaseModel):
interval: TrackTokensIntervalUnion1
r"""The reset interval (hour, day, week, month, etc.) or 'multiple' if combined from different intervals."""
resets_at: Nullable[float]
r"""Timestamp when the balance will next reset."""
interval_count: Optional[float] = None
r"""Number of intervals between resets (eg. 2 for bi-monthly)."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["interval_count"])
nullable_fields = set(["resets_at"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
class TrackTokensDeduction1TypedDict(TypedDict):
balance_id: str
r"""ID of the underlying balance row that was deducted from (customer_entitlement or rollover)."""
feature_id: str
r"""The feature this balance belongs to."""
plan_id: Nullable[str]
r"""ID of the plan/product this balance belongs to. Null when the balance can't be attributed to a single plan (e.g. it spans multiple)."""
reset: Nullable[TrackTokensReset1TypedDict]
r"""Reset configuration for the balance this deduction came from, or null if the balance doesn't reset."""
value: float
r"""Amount deducted from this balance. Positive when usage was consumed, negative when credit was restored (e.g. a refund via negative track value)."""
class TrackTokensDeduction1(BaseModel):
balance_id: str
r"""ID of the underlying balance row that was deducted from (customer_entitlement or rollover)."""
feature_id: str
r"""The feature this balance belongs to."""
plan_id: Nullable[str]
r"""ID of the plan/product this balance belongs to. Null when the balance can't be attributed to a single plan (e.g. it spans multiple)."""
reset: Nullable[TrackTokensReset1]
r"""Reset configuration for the balance this deduction came from, or null if the balance doesn't reset."""
value: float
r"""Amount deducted from this balance. Positive when usage was consumed, negative when credit was restored (e.g. a refund via negative track value)."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
m[k] = val
return m
class TrackTokensResponseBody1TypedDict(TypedDict):
r"""OK"""
customer_id: str
r"""The ID of the customer whose usage was tracked."""
value: float
r"""The amount of usage that was recorded."""
balance: Nullable[BalanceTypedDict]
r"""The updated balance for the tracked feature. Null if tracking by event_name that affects multiple features."""
entity_id: NotRequired[str]
r"""The ID of the entity, if entity-scoped tracking was performed."""
event_name: NotRequired[str]
r"""The event name that was tracked, if event_name was used instead of feature_id."""
balances: NotRequired[Dict[str, Nullable[BalanceTypedDict]]]
r"""Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature."""
deductions: NotRequired[List[TrackTokensDeduction1TypedDict]]
r"""Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling."""
class TrackTokensResponseBody1(BaseModel):
r"""OK"""
customer_id: str
r"""The ID of the customer whose usage was tracked."""
value: float
r"""The amount of usage that was recorded."""
balance: Nullable[Balance]
r"""The updated balance for the tracked feature. Null if tracking by event_name that affects multiple features."""
entity_id: Optional[str] = None
r"""The ID of the entity, if entity-scoped tracking was performed."""
event_name: Optional[str] = None
r"""The event name that was tracked, if event_name was used instead of feature_id."""
balances: Optional[Dict[str, Nullable[Balance]]] = None
r"""Map of feature_id to updated balance for the tracked feature and any related features (e.g. linked credit systems). Value is null when the customer has no balance for that feature."""
deductions: Optional[List[TrackTokensDeduction1]] = None
r"""Per-balance breakdown of what this event deducted. A single event can consume from multiple balance rows when credit systems or rollovers are involved; this surfaces each one so callers can build per-feature usage views without polling."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["entity_id", "event_name", "balances", "deductions"])
nullable_fields = set(["balance"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
TrackTokensResponseTypedDict = TypeAliasType(
"TrackTokensResponseTypedDict",
Union[TrackTokensResponseBody1TypedDict, TrackTokensResponseBody2TypedDict],
)
TrackTokensResponse = TypeAliasType(
"TrackTokensResponse", Union[TrackTokensResponseBody1, TrackTokensResponseBody2]
)

View File

@@ -46,7 +46,7 @@ class UpdateCustomerGlobals(BaseModel):
return m
UpdateCustomerIntervalRequest = Literal[
UpdateCustomerIntervalRequestBody = Literal[
"hour",
"day",
"week",
@@ -58,7 +58,7 @@ r"""The time interval for the purchase limit window."""
class UpdateCustomerPurchaseLimitRequestTypedDict(TypedDict):
r"""Optional rate limit to cap how often auto top-ups occur."""
interval: UpdateCustomerIntervalRequest
interval: UpdateCustomerIntervalRequestBody
r"""The time interval for the purchase limit window."""
limit: float
r"""Maximum number of auto top-ups allowed within the interval."""
@@ -69,7 +69,7 @@ class UpdateCustomerPurchaseLimitRequestTypedDict(TypedDict):
class UpdateCustomerPurchaseLimitRequest(BaseModel):
r"""Optional rate limit to cap how often auto top-ups occur."""
interval: UpdateCustomerIntervalRequest
interval: UpdateCustomerIntervalRequestBody
r"""The time interval for the purchase limit window."""
limit: float
@@ -994,10 +994,11 @@ UpdateCustomerType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class UpdateCustomerCreditSchemaTypedDict(TypedDict):
@@ -1015,6 +1016,44 @@ class UpdateCustomerCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class UpdateCustomerModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class UpdateCustomerModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class UpdateCustomerProviderMarkupsTypedDict(TypedDict):
markup: float
class UpdateCustomerProviderMarkups(BaseModel):
markup: float
class UpdateCustomerDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -1067,7 +1106,7 @@ class UpdateCustomerFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: UpdateCustomerType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -1076,6 +1115,14 @@ class UpdateCustomerFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[UpdateCustomerCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, UpdateCustomerModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, UpdateCustomerProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[UpdateCustomerDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -1090,7 +1137,7 @@ class UpdateCustomerFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: UpdateCustomerType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -1104,21 +1151,48 @@ class UpdateCustomerFeature(BaseModel):
credit_schema: Optional[List[UpdateCustomerCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, UpdateCustomerModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, UpdateCustomerProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[UpdateCustomerDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -458,10 +458,11 @@ UpdateEntityType = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class UpdateEntityCreditSchemaTypedDict(TypedDict):
@@ -479,6 +480,44 @@ class UpdateEntityCreditSchema(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class UpdateEntityModelMarkupsTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class UpdateEntityModelMarkups(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class UpdateEntityProviderMarkupsTypedDict(TypedDict):
markup: float
class UpdateEntityProviderMarkups(BaseModel):
markup: float
class UpdateEntityDisplayTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -531,7 +570,7 @@ class UpdateEntityFeatureTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: UpdateEntityType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -540,6 +579,14 @@ class UpdateEntityFeatureTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[UpdateEntityCreditSchemaTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[Nullable[Dict[str, UpdateEntityModelMarkupsTypedDict]]]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, UpdateEntityProviderMarkupsTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[UpdateEntityDisplayTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -554,7 +601,7 @@ class UpdateEntityFeature(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: UpdateEntityType
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -568,21 +615,48 @@ class UpdateEntityFeature(BaseModel):
credit_schema: Optional[List[UpdateEntityCreditSchema]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, UpdateEntityModelMarkups]] = UNSET
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[Dict[str, UpdateEntityProviderMarkups]] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[UpdateEntityDisplay] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -12,7 +12,7 @@ from autumn_sdk.types import (
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import List, Literal, Optional, Union
from typing import Dict, List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypedDict
@@ -44,22 +44,23 @@ class UpdateFeatureGlobals(BaseModel):
return m
UpdateFeatureTypeRequest = Literal[
UpdateFeatureTypeRequestBody = Literal[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
]
r"""The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system."""
class UpdateFeatureDisplayRequestTypedDict(TypedDict):
class UpdateFeatureDisplayRequestBodyTypedDict(TypedDict):
r"""Singular and plural display names for the feature in your user interface."""
singular: str
plural: str
class UpdateFeatureDisplayRequest(BaseModel):
class UpdateFeatureDisplayRequestBody(BaseModel):
r"""Singular and plural display names for the feature in your user interface."""
singular: str
@@ -67,30 +68,78 @@ class UpdateFeatureDisplayRequest(BaseModel):
plural: str
class UpdateFeatureCreditSchemaRequestTypedDict(TypedDict):
class UpdateFeatureCreditSchemaRequestBodyTypedDict(TypedDict):
metered_feature_id: str
credit_cost: float
class UpdateFeatureCreditSchemaRequest(BaseModel):
class UpdateFeatureCreditSchemaRequestBody(BaseModel):
metered_feature_id: str
credit_cost: float
class UpdateFeatureModelMarkupsRequestTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class UpdateFeatureModelMarkupsRequest(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class UpdateFeatureProviderMarkupsRequestTypedDict(TypedDict):
markup: float
class UpdateFeatureProviderMarkupsRequest(BaseModel):
markup: float
class UpdateFeatureParamsTypedDict(TypedDict):
feature_id: str
r"""The ID of the feature to update."""
name: NotRequired[str]
r"""The name of the feature."""
type: NotRequired[UpdateFeatureTypeRequest]
type: NotRequired[UpdateFeatureTypeRequestBody]
r"""The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system."""
consumable: NotRequired[bool]
r"""Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features."""
display: NotRequired[UpdateFeatureDisplayRequestTypedDict]
display: NotRequired[UpdateFeatureDisplayRequestBodyTypedDict]
r"""Singular and plural display names for the feature in your user interface."""
credit_schema: NotRequired[List[UpdateFeatureCreditSchemaRequestTypedDict]]
r"""A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features."""
credit_schema: NotRequired[List[UpdateFeatureCreditSchemaRequestBodyTypedDict]]
r"""A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead."""
model_markups: NotRequired[
Nullable[Dict[str, UpdateFeatureModelMarkupsRequestTypedDict]]
]
r"""Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration."""
default_markup: NotRequired[float]
r"""Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, UpdateFeatureProviderMarkupsRequestTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id."""
event_names: NotRequired[List[str]]
archived: NotRequired[bool]
r"""Whether the feature is archived. Archived features are hidden from the dashboard."""
@@ -105,17 +154,28 @@ class UpdateFeatureParams(BaseModel):
name: Optional[str] = None
r"""The name of the feature."""
type: Optional[UpdateFeatureTypeRequest] = None
type: Optional[UpdateFeatureTypeRequestBody] = None
r"""The type of the feature. 'single_use' features are consumed, like API calls, tokens, or messages. 'continuous_use' features are allocated, like seats, workspaces, or projects. 'credit_system' features are schemas that unify multiple 'single_use' features into a single credit system."""
consumable: Optional[bool] = None
r"""Whether this feature is consumable. A consumable feature is one that periodically resets and is consumed rather than allocated (like credits, API requests, etc.). Applicable only for 'metered' features."""
display: Optional[UpdateFeatureDisplayRequest] = None
display: Optional[UpdateFeatureDisplayRequestBody] = None
r"""Singular and plural display names for the feature in your user interface."""
credit_schema: Optional[List[UpdateFeatureCreditSchemaRequest]] = None
r"""A schema that maps 'single_use' feature IDs to credit costs. Applicable only for 'credit_system' features."""
credit_schema: Optional[List[UpdateFeatureCreditSchemaRequestBody]] = None
r"""A schema that maps 'single_use' feature IDs to credit costs. For classic credit systems only — AI credit systems use model_markups instead."""
model_markups: OptionalNullable[Dict[str, UpdateFeatureModelMarkupsRequest]] = UNSET
r"""Per-model markup overrides for AI credit systems. Maps model IDs to their markup configuration."""
default_markup: Optional[float] = None
r"""Default percentage markup for this AI credit system. Used when no model or provider markup applies. Use -100 to make usage free."""
provider_markups: OptionalNullable[
Dict[str, UpdateFeatureProviderMarkupsRequest]
] = UNSET
r"""Per-provider default markup percentages for AI credit systems. Provider keys match the first segment of model_id."""
event_names: Optional[List[str]] = None
@@ -134,20 +194,32 @@ class UpdateFeatureParams(BaseModel):
"consumable",
"display",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"event_names",
"archived",
"new_feature_id",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -158,10 +230,11 @@ UpdateFeatureTypeResponse = Union[
"boolean",
"metered",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
class UpdateFeatureCreditSchemaResponseTypedDict(TypedDict):
@@ -179,6 +252,44 @@ class UpdateFeatureCreditSchemaResponse(BaseModel):
r"""Credits consumed per unit of the metered feature."""
class UpdateFeatureModelMarkupsResponseTypedDict(TypedDict):
markup: NotRequired[float]
input_cost: NotRequired[float]
output_cost: NotRequired[float]
class UpdateFeatureModelMarkupsResponse(BaseModel):
markup: Optional[float] = None
input_cost: Optional[float] = None
output_cost: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["markup", "input_cost", "output_cost"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
class UpdateFeatureProviderMarkupsResponseTypedDict(TypedDict):
markup: float
class UpdateFeatureProviderMarkupsResponse(BaseModel):
markup: float
class UpdateFeatureDisplayResponseTypedDict(TypedDict):
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -231,7 +342,7 @@ class UpdateFeatureResponseTypedDict(TypedDict):
name: str
r"""Human-readable name displayed in the dashboard and billing UI."""
type: UpdateFeatureTypeResponse
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
archived: bool
@@ -240,6 +351,16 @@ class UpdateFeatureResponseTypedDict(TypedDict):
r"""Event names that trigger this feature's balance. Allows multiple features to respond to a single event."""
credit_schema: NotRequired[List[UpdateFeatureCreditSchemaResponseTypedDict]]
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: NotRequired[
Nullable[Dict[str, UpdateFeatureModelMarkupsResponseTypedDict]]
]
r"""Per-model markup overrides for AI credit systems."""
default_markup: NotRequired[float]
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: NotRequired[
Nullable[Dict[str, UpdateFeatureProviderMarkupsResponseTypedDict]]
]
r"""Per-provider default markup percentages for AI credit systems."""
display: NotRequired[UpdateFeatureDisplayResponseTypedDict]
r"""Display names for the feature in billing UI and customer-facing components."""
@@ -254,7 +375,7 @@ class UpdateFeatureResponse(BaseModel):
r"""Human-readable name displayed in the dashboard and billing UI."""
type: UpdateFeatureTypeResponse
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools."""
r"""Feature type: 'boolean' for on/off access, 'metered' for usage-tracked features, 'credit_system' for unified credit pools, 'ai_credit_system' for model-based token pricing."""
consumable: bool
r"""For metered features: true if usage resets periodically (API calls, credits), false if allocated persistently (seats, storage)."""
@@ -268,21 +389,52 @@ class UpdateFeatureResponse(BaseModel):
credit_schema: Optional[List[UpdateFeatureCreditSchemaResponse]] = None
r"""For credit_system features: maps metered features to their credit costs."""
model_markups: OptionalNullable[Dict[str, UpdateFeatureModelMarkupsResponse]] = (
UNSET
)
r"""Per-model markup overrides for AI credit systems."""
default_markup: Optional[float] = None
r"""Default percentage markup for AI credit systems. Use -100 to make usage free."""
provider_markups: OptionalNullable[
Dict[str, UpdateFeatureProviderMarkupsResponse]
] = UNSET
r"""Per-provider default markup percentages for AI credit systems."""
display: Optional[UpdateFeatureDisplayResponse] = None
r"""Display names for the feature in billing UI and customer-facing components."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["event_names", "credit_schema", "display"])
optional_fields = set(
[
"event_names",
"credit_schema",
"model_markups",
"default_markup",
"provider_markups",
"display",
]
)
nullable_fields = set(["model_markups", "provider_markups"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m

View File

@@ -12,7 +12,7 @@ from autumn_sdk.types import (
from autumn_sdk.utils import FieldMetadata, HeaderMetadata
import pydantic
from pydantic import model_serializer
from typing import Any, List, Literal, Optional, Union
from typing import List, Literal, Optional, Union
from typing_extensions import Annotated, NotRequired, TypeAliasType, TypedDict
@@ -44,7 +44,7 @@ class UpdatePlanGlobals(BaseModel):
return m
UpdatePlanPriceIntervalRequest = Literal[
UpdatePlanPriceIntervalRequestBody = Literal[
"one_off",
"week",
"month",
@@ -60,7 +60,7 @@ class UpdatePlanBasePriceTypedDict(TypedDict):
amount: float
r"""Base price amount for the plan."""
interval: UpdatePlanPriceIntervalRequest
interval: UpdatePlanPriceIntervalRequestBody
r"""Billing interval (e.g. 'month', 'year')."""
interval_count: NotRequired[float]
r"""Number of intervals per billing cycle. Defaults to 1."""
@@ -72,7 +72,7 @@ class UpdatePlanBasePrice(BaseModel):
amount: float
r"""Base price amount for the plan."""
interval: UpdatePlanPriceIntervalRequest
interval: UpdatePlanPriceIntervalRequestBody
r"""Billing interval (e.g. 'month', 'year')."""
interval_count: Optional[float] = None
@@ -95,7 +95,7 @@ class UpdatePlanBasePrice(BaseModel):
return m
UpdatePlanResetIntervalRequest = Literal[
UpdatePlanResetIntervalRequestBody = Literal[
"one_off",
"minute",
"hour",
@@ -109,19 +109,19 @@ UpdatePlanResetIntervalRequest = Literal[
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
class UpdatePlanResetRequestTypedDict(TypedDict):
class UpdatePlanResetRequestBodyTypedDict(TypedDict):
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
interval: UpdatePlanResetIntervalRequest
interval: UpdatePlanResetIntervalRequestBody
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
interval_count: NotRequired[float]
r"""Number of intervals between resets. Defaults to 1."""
class UpdatePlanResetRequest(BaseModel):
class UpdatePlanResetRequestBody(BaseModel):
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
interval: UpdatePlanResetIntervalRequest
interval: UpdatePlanResetIntervalRequestBody
r"""Interval at which balance resets (e.g. 'month', 'year'). For consumable features only."""
interval_count: Optional[float] = None
@@ -144,20 +144,22 @@ class UpdatePlanResetRequest(BaseModel):
return m
UpdatePlanToTypedDict = TypeAliasType("UpdatePlanToTypedDict", Union[float, str])
UpdatePlanToRequestBodyTypedDict = TypeAliasType(
"UpdatePlanToRequestBodyTypedDict", Union[float, str]
)
UpdatePlanTo = TypeAliasType("UpdatePlanTo", Union[float, str])
UpdatePlanToRequestBody = TypeAliasType("UpdatePlanToRequestBody", Union[float, str])
class UpdatePlanTierTypedDict(TypedDict):
to: UpdatePlanToTypedDict
class UpdatePlanTierRequestBodyTypedDict(TypedDict):
to: UpdatePlanToRequestBodyTypedDict
amount: NotRequired[float]
flat_amount: NotRequired[float]
class UpdatePlanTier(BaseModel):
to: UpdatePlanTo
class UpdatePlanTierRequestBody(BaseModel):
to: UpdatePlanToRequestBody
amount: Optional[float] = None
@@ -180,13 +182,13 @@ class UpdatePlanTier(BaseModel):
return m
UpdatePlanTierBehaviorRequest = Literal[
UpdatePlanTierBehaviorRequestBody = Literal[
"graduated",
"volume",
]
UpdatePlanItemPriceIntervalRequest = Literal[
UpdatePlanItemPriceIntervalRequestBody = Literal[
"one_off",
"week",
"month",
@@ -197,49 +199,49 @@ UpdatePlanItemPriceIntervalRequest = Literal[
r"""Billing interval. For consumable features, should match reset.interval."""
UpdatePlanBillingMethodRequest = Literal[
UpdatePlanBillingMethodRequestBody = Literal[
"prepaid",
"usage_based",
]
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
class UpdatePlanPriceRequestTypedDict(TypedDict):
class UpdatePlanPriceRequestBodyTypedDict(TypedDict):
r"""Pricing for usage beyond included units. Omit for free features."""
interval: UpdatePlanItemPriceIntervalRequest
interval: UpdatePlanItemPriceIntervalRequestBody
r"""Billing interval. For consumable features, should match reset.interval."""
billing_method: UpdatePlanBillingMethodRequest
billing_method: UpdatePlanBillingMethodRequestBody
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
amount: NotRequired[float]
r"""Price per billing_units after included usage. Either 'amount' or 'tiers' is required."""
tiers: NotRequired[List[UpdatePlanTierTypedDict]]
tiers: NotRequired[List[UpdatePlanTierRequestBodyTypedDict]]
r"""Tiered pricing. Either 'amount' or 'tiers' is required."""
tier_behavior: NotRequired[UpdatePlanTierBehaviorRequest]
tier_behavior: NotRequired[UpdatePlanTierBehaviorRequestBody]
interval_count: NotRequired[float]
r"""Number of intervals per billing cycle. Defaults to 1."""
billing_units: NotRequired[float]
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: NotRequired[float]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: NotRequired[Nullable[float]]
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
class UpdatePlanPriceRequest(BaseModel):
class UpdatePlanPriceRequestBody(BaseModel):
r"""Pricing for usage beyond included units. Omit for free features."""
interval: UpdatePlanItemPriceIntervalRequest
interval: UpdatePlanItemPriceIntervalRequestBody
r"""Billing interval. For consumable features, should match reset.interval."""
billing_method: UpdatePlanBillingMethodRequest
billing_method: UpdatePlanBillingMethodRequestBody
r"""'prepaid' for upfront payment (seats), 'usage_based' for pay-as-you-go."""
amount: Optional[float] = None
r"""Price per billing_units after included usage. Either 'amount' or 'tiers' is required."""
tiers: Optional[List[UpdatePlanTier]] = None
tiers: Optional[List[UpdatePlanTierRequestBody]] = None
r"""Tiered pricing. Either 'amount' or 'tiers' is required."""
tier_behavior: Optional[UpdatePlanTierBehaviorRequest] = None
tier_behavior: Optional[UpdatePlanTierBehaviorRequestBody] = None
interval_count: Optional[float] = 1
r"""Number of intervals per billing cycle. Defaults to 1."""
@@ -247,8 +249,8 @@ class UpdatePlanPriceRequest(BaseModel):
billing_units: Optional[float] = 1
r"""Units per price increment. Usage is rounded UP when billed (e.g. billing_units=100 means 101 rounds to 200)."""
max_purchase: Optional[float] = None
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total."""
max_purchase: OptionalNullable[float] = UNSET
r"""Max units purchasable beyond included. E.g. included=100, max_purchase=300 allows 400 total. Null for no limit."""
@model_serializer(mode="wrap")
def serialize_model(self, handler):
@@ -262,15 +264,24 @@ class UpdatePlanPriceRequest(BaseModel):
"max_purchase",
]
)
nullable_fields = set(["max_purchase"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
is_nullable_and_explicitly_set = (
k in nullable_fields
and (self.__pydantic_fields_set__.intersection({n})) # pylint: disable=no-member
)
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
if (
val is not None
or k not in optional_fields
or is_nullable_and_explicitly_set
):
m[k] = val
return m
@@ -314,17 +325,17 @@ class UpdatePlanProration(BaseModel):
r"""Credit behavior when quantity decreases mid-cycle."""
UpdatePlanExpiryDurationTypeRequest = Literal[
UpdatePlanExpiryDurationTypeRequestBody = Literal[
"month",
"forever",
]
r"""When rolled over units expire."""
class UpdatePlanRolloverRequestTypedDict(TypedDict):
class UpdatePlanRolloverRequestBodyTypedDict(TypedDict):
r"""Rollover config for unused units. If set, unused included units carry over."""
expiry_duration_type: UpdatePlanExpiryDurationTypeRequest
expiry_duration_type: UpdatePlanExpiryDurationTypeRequestBody
r"""When rolled over units expire."""
max: NotRequired[float]
r"""Max rollover units. Omit for unlimited rollover."""
@@ -334,10 +345,10 @@ class UpdatePlanRolloverRequestTypedDict(TypedDict):
r"""Number of periods before expiry."""
class UpdatePlanRolloverRequest(BaseModel):
class UpdatePlanRolloverRequestBody(BaseModel):
r"""Rollover config for unused units. If set, unused included units carry over."""
expiry_duration_type: UpdatePlanExpiryDurationTypeRequest
expiry_duration_type: UpdatePlanExpiryDurationTypeRequestBody
r"""When rolled over units expire."""
max: Optional[float] = None
@@ -375,13 +386,13 @@ class UpdatePlanPlanItemTypedDict(TypedDict):
r"""Number of free units included. Balance resets to this each interval for consumable features."""
unlimited: NotRequired[bool]
r"""If true, customer has unlimited access to this feature."""
reset: NotRequired[UpdatePlanResetRequestTypedDict]
reset: NotRequired[UpdatePlanResetRequestBodyTypedDict]
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
price: NotRequired[UpdatePlanPriceRequestTypedDict]
price: NotRequired[UpdatePlanPriceRequestBodyTypedDict]
r"""Pricing for usage beyond included units. Omit for free features."""
proration: NotRequired[UpdatePlanProrationTypedDict]
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
rollover: NotRequired[UpdatePlanRolloverRequestTypedDict]
rollover: NotRequired[UpdatePlanRolloverRequestBodyTypedDict]
r"""Rollover config for unused units. If set, unused included units carry over."""
@@ -397,16 +408,16 @@ class UpdatePlanPlanItem(BaseModel):
unlimited: Optional[bool] = None
r"""If true, customer has unlimited access to this feature."""
reset: Optional[UpdatePlanResetRequest] = None
reset: Optional[UpdatePlanResetRequestBody] = None
r"""Reset configuration for consumable features. Omit for non-consumable features like seats."""
price: Optional[UpdatePlanPriceRequest] = None
price: Optional[UpdatePlanPriceRequestBody] = None
r"""Pricing for usage beyond included units. Omit for free features."""
proration: Optional[UpdatePlanProration] = None
r"""Proration settings for prepaid features. Controls mid-cycle quantity change billing."""
rollover: Optional[UpdatePlanRolloverRequest] = None
rollover: Optional[UpdatePlanRolloverRequestBody] = None
r"""Rollover config for unused units. If set, unused included units carry over."""
@model_serializer(mode="wrap")
@@ -543,6 +554,7 @@ class UpdatePlanParamsTypedDict(TypedDict):
archived: NotRequired[bool]
new_plan_id: NotRequired[str]
r"""The new ID to use for the plan. Can only be updated if the plan has not been used by any customers."""
disable_version: NotRequired[bool]
class UpdatePlanParams(BaseModel):
@@ -584,6 +596,8 @@ class UpdatePlanParams(BaseModel):
new_plan_id: Optional[str] = None
r"""The new ID to use for the plan. Can only be updated if the plan has not been used by any customers."""
disable_version: Optional[bool] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(
@@ -601,6 +615,7 @@ class UpdatePlanParams(BaseModel):
"version",
"archived",
"new_plan_id",
"disable_version",
]
)
nullable_fields = set(["price", "free_trial"])
@@ -723,6 +738,7 @@ UpdatePlanType = Union[
"single_use",
"continuous_use",
"credit_system",
"ai_credit_system",
],
UnrecognizedStr,
]
@@ -871,6 +887,44 @@ class UpdatePlanResetResponse(BaseModel):
return m
UpdatePlanToResponseTypedDict = TypeAliasType(
"UpdatePlanToResponseTypedDict", Union[float, str]
)
UpdatePlanToResponse = TypeAliasType("UpdatePlanToResponse", Union[float, str])
class UpdatePlanTierResponseTypedDict(TypedDict):
to: UpdatePlanToResponseTypedDict
amount: float
flat_amount: NotRequired[float]
class UpdatePlanTierResponse(BaseModel):
to: UpdatePlanToResponse
amount: float
flat_amount: Optional[float] = None
@model_serializer(mode="wrap")
def serialize_model(self, handler):
optional_fields = set(["flat_amount"])
serialized = handler(self)
m = {}
for n, f in type(self).model_fields.items():
k = f.alias or n
val = serialized.get(k, serialized.get(n))
if val != UNSET_SENTINEL:
if val is not None or k not in optional_fields:
m[k] = val
return m
UpdatePlanTierBehaviorResponse = Union[
Literal[
"graduated",
@@ -915,7 +969,7 @@ class UpdatePlanItemPriceResponseTypedDict(TypedDict):
r"""Maximum units a customer can purchase beyond included. E.g. if included=100 and max_purchase=300, customer can use up to 400 total before usage is capped. Null for no limit."""
amount: NotRequired[float]
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: NotRequired[List[Nullable[Any]]]
tiers: NotRequired[List[UpdatePlanTierResponseTypedDict]]
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: NotRequired[UpdatePlanTierBehaviorResponse]
interval_count: NotRequired[float]
@@ -938,7 +992,7 @@ class UpdatePlanItemPriceResponse(BaseModel):
amount: Optional[float] = None
r"""Price per billing_units after included usage is consumed. Mutually exclusive with tiers."""
tiers: Optional[List[Nullable[Any]]] = None
tiers: Optional[List[UpdatePlanTierResponse]] = None
r"""Tiered pricing configuration. Each tier's 'to' INCLUDES the included amount. Either 'tiers' or 'amount' is required."""
tier_behavior: Optional[UpdatePlanTierBehaviorResponse] = None

View File

@@ -19,7 +19,10 @@ class Plans(BaseSDK):
add_on: Optional[bool] = False,
auto_enable: Optional[bool] = False,
price: Optional[
Union[models.CreatePlanPriceRequest, models.CreatePlanPriceRequestTypedDict]
Union[
models.CreatePlanPriceRequestBody,
models.CreatePlanPriceRequestBodyTypedDict,
]
] = None,
items: Optional[
Union[
@@ -81,7 +84,7 @@ class Plans(BaseSDK):
add_on=add_on,
auto_enable=auto_enable,
price=utils.get_pydantic_model(
price, Optional[models.CreatePlanPriceRequest]
price, Optional[models.CreatePlanPriceRequestBody]
),
items=utils.get_pydantic_model(
items, Optional[List[models.CreatePlanPlanItem]]
@@ -164,7 +167,10 @@ class Plans(BaseSDK):
add_on: Optional[bool] = False,
auto_enable: Optional[bool] = False,
price: Optional[
Union[models.CreatePlanPriceRequest, models.CreatePlanPriceRequestTypedDict]
Union[
models.CreatePlanPriceRequestBody,
models.CreatePlanPriceRequestBodyTypedDict,
]
] = None,
items: Optional[
Union[
@@ -226,7 +232,7 @@ class Plans(BaseSDK):
add_on=add_on,
auto_enable=auto_enable,
price=utils.get_pydantic_model(
price, Optional[models.CreatePlanPriceRequest]
price, Optional[models.CreatePlanPriceRequestBody]
),
items=utils.get_pydantic_model(
items, Optional[List[models.CreatePlanPlanItem]]
@@ -718,6 +724,7 @@ class Plans(BaseSDK):
version: Optional[float] = None,
archived: Optional[bool] = False,
new_plan_id: Optional[str] = None,
disable_version: Optional[bool] = None,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
@@ -743,6 +750,7 @@ class Plans(BaseSDK):
:param version:
:param archived:
:param new_plan_id: The new ID to use for the plan. Can only be updated if the plan has not been used by any customers.
:param disable_version:
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
:param timeout_ms: Override the default request timeout configuration for this method in milliseconds
@@ -781,6 +789,7 @@ class Plans(BaseSDK):
version=version,
archived=archived,
new_plan_id=new_plan_id,
disable_version=disable_version,
)
req = self._build_request(
@@ -875,6 +884,7 @@ class Plans(BaseSDK):
version: Optional[float] = None,
archived: Optional[bool] = False,
new_plan_id: Optional[str] = None,
disable_version: Optional[bool] = None,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
@@ -900,6 +910,7 @@ class Plans(BaseSDK):
:param version:
:param archived:
:param new_plan_id: The new ID to use for the plan. Can only be updated if the plan has not been used by any customers.
:param disable_version:
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
:param timeout_ms: Override the default request timeout configuration for this method in milliseconds
@@ -938,6 +949,7 @@ class Plans(BaseSDK):
version=version,
archived=archived,
new_plan_id=new_plan_id,
disable_version=disable_version,
)
req = self._build_request_async(

View File

@@ -684,6 +684,260 @@ class Autumn(BaseSDK):
raise errors.AutumnDefaultError("Unexpected response received", http_res)
def track_tokens(
self,
*,
customer_id: str,
model_id: str,
input_tokens: int,
output_tokens: int,
entity_id: Optional[str] = None,
feature_id: Optional[str] = None,
cache_read_tokens: Optional[int] = None,
cache_write_tokens: Optional[int] = None,
audio_input_tokens: Optional[int] = None,
audio_output_tokens: Optional[int] = None,
reasoning_tokens: Optional[int] = None,
properties: Optional[Dict[str, Any]] = None,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
) -> models.TrackTokensResponse:
r"""Records AI token usage for a customer and returns the updated AI credit balance.
Use this after an LLM request when you have input and output token counts. Autumn converts token usage to a dollar amount using the configured model pricing and markup, then tracks that value against the customer's AI credit system.
:param customer_id: The ID of the customer.
:param model_id: The AI model as '<provider>/<model>' (e.g. 'anthropic/claude-opus-4-8', 'openrouter/openai/gpt-4o'). The provider is the first path segment and must match a provider + model key in models.dev.
:param input_tokens: Number of non-cached text input tokens consumed. Exclusive of cache and audio token pools.
:param output_tokens: Number of text output tokens consumed. Exclusive of the reasoning and audio output pools.
:param entity_id: The ID of the entity for entity-scoped balances.
:param feature_id: The ID of the AI credit system feature. Auto-detected from the customer's entitlements if omitted — only required when a customer has multiple AI credit systems.
:param cache_read_tokens: Number of cached input tokens read.
:param cache_write_tokens: Number of input tokens written to the cache.
:param audio_input_tokens: Number of audio input tokens consumed.
:param audio_output_tokens: Number of audio output tokens generated.
:param reasoning_tokens: Number of reasoning tokens generated.
:param properties: Additional properties to attach to this usage event.
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
:param timeout_ms: Override the default request timeout configuration for this method in milliseconds
:param http_headers: Additional headers to set or replace on requests.
"""
base_url = None
url_variables = None
if timeout_ms is None:
timeout_ms = self.sdk_configuration.timeout_ms
if server_url is not None:
base_url = server_url
else:
base_url = self._get_url(base_url, url_variables)
request = models.TrackTokensParams(
customer_id=customer_id,
entity_id=entity_id,
feature_id=feature_id,
model_id=model_id,
input_tokens=input_tokens,
output_tokens=output_tokens,
cache_read_tokens=cache_read_tokens,
cache_write_tokens=cache_write_tokens,
audio_input_tokens=audio_input_tokens,
audio_output_tokens=audio_output_tokens,
reasoning_tokens=reasoning_tokens,
properties=properties,
)
req = self._build_request(
method="POST",
path="/v1/balances.track_tokens",
base_url=base_url,
url_variables=url_variables,
request=request,
request_body_required=True,
request_has_path_params=False,
request_has_query_params=True,
user_agent_header="user-agent",
accept_header_value="application/json",
http_headers=http_headers,
_globals=models.TrackTokensGlobals(
x_api_version=self.sdk_configuration.globals.x_api_version,
),
security=self.sdk_configuration.security,
get_serialized_body=lambda: utils.serialize_request_body(
request, False, False, "json", models.TrackTokensParams
),
allow_empty_value=None,
timeout_ms=timeout_ms,
)
if retries == UNSET:
if self.sdk_configuration.retry_config is not UNSET:
retries = self.sdk_configuration.retry_config
retry_config = None
if isinstance(retries, utils.RetryConfig):
retry_config = (retries, ["429", "500", "502", "503", "504"])
http_res = self.do_request(
hook_ctx=HookContext(
config=self.sdk_configuration,
base_url=base_url or "",
operation_id="trackTokens",
oauth2_scopes=None,
security_source=self.sdk_configuration.security,
),
request=req,
is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c),
retry_config=retry_config,
)
if utils.match_response(http_res, "200", "application/json"):
return unmarshal_json_response(models.TrackTokensResponseBody1, http_res)
if utils.match_response(http_res, "202", "application/json"):
return unmarshal_json_response(models.TrackTokensResponseBody2, http_res)
if utils.match_response(http_res, "4XX", "*"):
http_res_text = utils.stream_to_text(http_res)
raise errors.AutumnDefaultError(
"API error occurred", http_res, http_res_text
)
if utils.match_response(http_res, "5XX", "*"):
http_res_text = utils.stream_to_text(http_res)
raise errors.AutumnDefaultError(
"API error occurred", http_res, http_res_text
)
raise errors.AutumnDefaultError("Unexpected response received", http_res)
async def track_tokens_async(
self,
*,
customer_id: str,
model_id: str,
input_tokens: int,
output_tokens: int,
entity_id: Optional[str] = None,
feature_id: Optional[str] = None,
cache_read_tokens: Optional[int] = None,
cache_write_tokens: Optional[int] = None,
audio_input_tokens: Optional[int] = None,
audio_output_tokens: Optional[int] = None,
reasoning_tokens: Optional[int] = None,
properties: Optional[Dict[str, Any]] = None,
retries: OptionalNullable[utils.RetryConfig] = UNSET,
server_url: Optional[str] = None,
timeout_ms: Optional[int] = None,
http_headers: Optional[Mapping[str, str]] = None,
) -> models.TrackTokensResponse:
r"""Records AI token usage for a customer and returns the updated AI credit balance.
Use this after an LLM request when you have input and output token counts. Autumn converts token usage to a dollar amount using the configured model pricing and markup, then tracks that value against the customer's AI credit system.
:param customer_id: The ID of the customer.
:param model_id: The AI model as '<provider>/<model>' (e.g. 'anthropic/claude-opus-4-8', 'openrouter/openai/gpt-4o'). The provider is the first path segment and must match a provider + model key in models.dev.
:param input_tokens: Number of non-cached text input tokens consumed. Exclusive of cache and audio token pools.
:param output_tokens: Number of text output tokens consumed. Exclusive of the reasoning and audio output pools.
:param entity_id: The ID of the entity for entity-scoped balances.
:param feature_id: The ID of the AI credit system feature. Auto-detected from the customer's entitlements if omitted — only required when a customer has multiple AI credit systems.
:param cache_read_tokens: Number of cached input tokens read.
:param cache_write_tokens: Number of input tokens written to the cache.
:param audio_input_tokens: Number of audio input tokens consumed.
:param audio_output_tokens: Number of audio output tokens generated.
:param reasoning_tokens: Number of reasoning tokens generated.
:param properties: Additional properties to attach to this usage event.
:param retries: Override the default retry configuration for this method
:param server_url: Override the default server URL for this method
:param timeout_ms: Override the default request timeout configuration for this method in milliseconds
:param http_headers: Additional headers to set or replace on requests.
"""
base_url = None
url_variables = None
if timeout_ms is None:
timeout_ms = self.sdk_configuration.timeout_ms
if server_url is not None:
base_url = server_url
else:
base_url = self._get_url(base_url, url_variables)
request = models.TrackTokensParams(
customer_id=customer_id,
entity_id=entity_id,
feature_id=feature_id,
model_id=model_id,
input_tokens=input_tokens,
output_tokens=output_tokens,
cache_read_tokens=cache_read_tokens,
cache_write_tokens=cache_write_tokens,
audio_input_tokens=audio_input_tokens,
audio_output_tokens=audio_output_tokens,
reasoning_tokens=reasoning_tokens,
properties=properties,
)
req = self._build_request_async(
method="POST",
path="/v1/balances.track_tokens",
base_url=base_url,
url_variables=url_variables,
request=request,
request_body_required=True,
request_has_path_params=False,
request_has_query_params=True,
user_agent_header="user-agent",
accept_header_value="application/json",
http_headers=http_headers,
_globals=models.TrackTokensGlobals(
x_api_version=self.sdk_configuration.globals.x_api_version,
),
security=self.sdk_configuration.security,
get_serialized_body=lambda: utils.serialize_request_body(
request, False, False, "json", models.TrackTokensParams
),
allow_empty_value=None,
timeout_ms=timeout_ms,
)
if retries == UNSET:
if self.sdk_configuration.retry_config is not UNSET:
retries = self.sdk_configuration.retry_config
retry_config = None
if isinstance(retries, utils.RetryConfig):
retry_config = (retries, ["429", "500", "502", "503", "504"])
http_res = await self.do_request_async(
hook_ctx=HookContext(
config=self.sdk_configuration,
base_url=base_url or "",
operation_id="trackTokens",
oauth2_scopes=None,
security_source=self.sdk_configuration.security,
),
request=req,
is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c),
retry_config=retry_config,
)
if utils.match_response(http_res, "200", "application/json"):
return unmarshal_json_response(models.TrackTokensResponseBody1, http_res)
if utils.match_response(http_res, "202", "application/json"):
return unmarshal_json_response(models.TrackTokensResponseBody2, http_res)
if utils.match_response(http_res, "4XX", "*"):
http_res_text = await utils.stream_to_text_async(http_res)
raise errors.AutumnDefaultError(
"API error occurred", http_res, http_res_text
)
if utils.match_response(http_res, "5XX", "*"):
http_res_text = await utils.stream_to_text_async(http_res)
raise errors.AutumnDefaultError(
"API error occurred", http_res, http_res_text
)
raise errors.AutumnDefaultError("Unexpected response received", http_res)
def batch_track(
self,
*,

View File

@@ -22,7 +22,8 @@
"packages/autumn-js",
"packages/openapi",
"packages/ksuid",
"packages/stripe-sync"
"packages/stripe-sync",
"packages/ai-sdk"
],
"catalog": {
"stripe": "19.3.0-beta.1",
@@ -50,9 +51,7 @@
}
},
"overrides": {
"@better-auth/core": "1.6.5",
"@better-auth/passkey": "1.6.5",
"better-auth": "1.6.5",
"@modelcontextprotocol/sdk": "1.29.0",
"@isaacs/brace-expansion": "5.0.1",
"fast-xml-parser": "5.3.4",
@@ -140,7 +139,7 @@
"site": "cd apps/website && bun dev && cd ../..",
"docs": "bun -F @autumn/docs dev",
"docs:build": "bun -F @autumn/docs build",
"ts": "turbo run ts --filter=@autumn/server --filter=autumn-js --filter=@autumn/openapi --filter=atmn --filter=checkout --filter=@autumn/auth --filter=@autumn/mcp --filter=@autumn/leaf",
"ts": "turbo run ts --filter=@autumn/server --filter=autumn-js --filter=@autumn/openapi --filter=atmn --filter=checkout --filter=@useautumn/ai-sdk --filter=@autumn/auth --filter=@autumn/mcp --filter=@autumn/leaf",
"kill:ts": "while pgrep -f tsgo > /dev/null; do pkill -9 -f tsgo; sleep 0.1; done",
"atmn:build": "bun -F atmn build",
"openapi:ts": "bun -F @autumn/openapi ts",

253
packages/ai-sdk/bun.lock Normal file
View File

@@ -0,0 +1,253 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@useautumn/ai-sdk",
"devDependencies": {
"@types/node": "^24.9.1",
"tsup": "^8.4.0",
"typescript": "^5.8.3",
},
"peerDependencies": {
"ai": "^6.0.116",
"autumn-js": "*",
},
},
},
"packages": {
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"autumn-js": ["autumn-js@1.0.5", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.3.17", "better-call": "^1.0.12", "express": "^5.2.1", "hono": "^4.0.0", "next": "^14.0.0 || ^15.0.0", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["better-auth", "better-call", "express", "hono", "next", "react"] }, "sha512-Xh4hqx7EO+hilVjCuilLjUt5iw6RP8vxKmkKmwwHM8WhrHH4StAol17Qis9FtZ/ixeHfe4TOFojzgLhP74kETw=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="],
"esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="],
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
"query-string": ["query-string@9.3.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"rou3": ["rou3@0.6.3", "", {}, "sha512-1HSG1ENTj7Kkm5muMnXuzzfdDOf7CFnbSYFA+H3Fp/rB9lOCxCPgy1jlZxTKyFoC5jJay8Mmc+VbPLYRjzYLrA=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
}
}

View File

@@ -0,0 +1,39 @@
{
"name": "@useautumn/ai-sdk",
"version": "0.0.1",
"type": "module",
"description": "AI SDK for Autumn",
"main": "./dist/sdk/index.cjs",
"module": "./dist/sdk/index.js",
"types": "./dist/sdk/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE.md"
],
"exports": {
".": {
"types": "./dist/sdk/index.d.ts",
"require": "./dist/sdk/index.cjs",
"import": "./dist/sdk/index.js"
}
},
"scripts": {
"ts": "tsgo --noEmit --skipLibCheck",
"test": "bun test tests/unit",
"build": "rm -rf dist && tsup",
"prepublishOnly": "bun run build"
},
"dependencies": {
"@ai-sdk/provider": "^3.0.0"
},
"peerDependencies": {
"ai": "^6.0.116",
"autumn-js": "*"
},
"devDependencies": {
"@types/node": "^24.9.1",
"tsup": "^8.4.0",
"typescript": "^5.8.3"
}
}

View File

@@ -0,0 +1,107 @@
import type { LanguageModelV3 } from "@ai-sdk/provider";
import { type LanguageModelMiddleware, wrapLanguageModel } from "ai";
import { normalizeUsage, type TokenPools, type UsageLike } from "./usage.js";
export type { TokenPools, UsageLike } from "./usage.js";
type TrackTokensParams = TokenPools & {
customerId: string;
modelId: string;
featureId?: string;
entityId?: string;
properties?: Record<string, unknown>;
};
/** Structural view of the autumn-js client; older versions may not ship balances.trackTokens. */
export type AutumnClient = {
balances?: {
trackTokens?: (params: TrackTokensParams) => Promise<unknown>;
};
};
export type WithAutumnOptions = {
/** Autumn SDK client instance. */
autumn: AutumnClient;
/** The AI SDK language model to wrap. */
model: LanguageModelV3;
/** The Autumn customer ID to attribute usage to. */
customerId: string;
/** Override the provider prefix used in the model name (e.g. "openrouter", "custom"). Falls back to `model.provider`. */
providerId?: string;
/** Target a specific AI credit system feature. Auto-detected if omitted. */
featureId?: string;
/** Entity ID for entity-scoped balance tracking. */
entityId?: string;
/** Additional properties to attach to each usage event. */
properties?: Record<string, unknown>;
};
export const withAutumn = ({
autumn,
model,
customerId,
providerId,
featureId,
entityId,
properties,
}: WithAutumnOptions): LanguageModelV3 => {
const modelName = `${providerId ?? model.provider}/${model.modelId}`;
const trackUsage = async (usage: UsageLike) => {
try {
const trackTokens = autumn.balances?.trackTokens;
if (!trackTokens) {
throw new Error(
"autumn-js client does not support balances.trackTokens — upgrade autumn-js.",
);
}
await trackTokens({
...normalizeUsage(usage, modelName),
customerId,
modelId: modelName,
featureId,
entityId,
properties,
});
} catch (error) {
console.error("[Autumn Tracking] Failed to track usage:", error);
}
};
const middleware: LanguageModelMiddleware = {
specificationVersion: "v3",
wrapGenerate: async ({ doGenerate }) => {
const result = await doGenerate();
await trackUsage(result.usage as UsageLike);
return result;
},
wrapStream: async ({ doStream }) => {
const { stream, ...rest } = await doStream();
let trackingPromise: Promise<void> | undefined;
type StreamChunk = typeof stream extends ReadableStream<infer T>
? T
: never;
const transformStream = new TransformStream<StreamChunk, StreamChunk>({
transform(chunk, controller) {
if (chunk.type === "finish" && chunk.usage) {
trackingPromise = trackUsage(chunk.usage as UsageLike);
}
controller.enqueue(chunk);
},
async flush() {
await trackingPromise;
},
});
return {
stream: stream.pipeThrough(transformStream),
...rest,
};
},
};
return wrapLanguageModel({ model, middleware });
};

View File

@@ -0,0 +1,117 @@
type NestedTokens = {
total?: number | null;
noCache?: number | null;
cacheRead?: number | null;
cacheWrite?: number | null;
text?: number | null;
reasoning?: number | null;
};
type LegacyCount = number | { total?: number | null } | null;
/** Lenient view over AI SDK usage shapes: nested V3 counts, flat counts with token details, and legacy prompt/completion counts. */
export type UsageLike = {
inputTokens?: number | NestedTokens | null;
outputTokens?: number | NestedTokens | null;
promptTokens?: LegacyCount;
completionTokens?: LegacyCount;
inputTokenDetails?: {
noCacheTokens?: number | null;
cacheReadTokens?: number | null;
cacheWriteTokens?: number | null;
} | null;
outputTokenDetails?: {
textTokens?: number | null;
reasoningTokens?: number | null;
} | null;
cachedInputTokens?: number | null;
reasoningTokens?: number | null;
};
export type TokenPools = {
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheWriteTokens: number;
reasoningTokens: number;
};
const flatCount = (value: LegacyCount | undefined): number | undefined =>
typeof value === "number" ? value : (value?.total ?? undefined);
const isNested = (
value: number | NestedTokens | null | undefined,
): value is NestedTokens => value != null && typeof value === "object";
const toParts = (usage: UsageLike) => {
const input = usage.inputTokens;
const output = usage.outputTokens;
if (isNested(input)) {
const out = isNested(output) ? output : undefined;
return {
cacheRead: input.cacheRead ?? 0,
cacheWrite: input.cacheWrite ?? 0,
reasoning: out?.reasoning ?? 0,
textInput: input.noCache,
totalInput: input.total,
textOutput: out?.text,
totalOutput: out?.total,
};
}
return {
cacheRead:
usage.inputTokenDetails?.cacheReadTokens ?? usage.cachedInputTokens ?? 0,
cacheWrite: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
reasoning:
usage.outputTokenDetails?.reasoningTokens ?? usage.reasoningTokens ?? 0,
textInput: usage.inputTokenDetails?.noCacheTokens,
totalInput:
typeof input === "number" ? input : flatCount(usage.promptTokens),
textOutput: usage.outputTokenDetails?.textTokens,
totalOutput:
typeof output === "number" ? output : flatCount(usage.completionTokens),
};
};
const clamp = (value: number) => Math.max(0, value);
/** Splits provider usage into exclusive token pools; throws if the provider returned no usable counts. */
export const normalizeUsage = (
usage: UsageLike,
modelName: string,
): TokenPools => {
const parts = toParts(usage);
const required = (
value: number | null | undefined,
label: string,
): number => {
if (value == null) {
throw new Error(
`[Autumn] ${label} token usage was not returned by the model provider (${modelName}). This provider may not support usage tracking.`,
);
}
return value;
};
const textInput =
parts.textInput ??
(parts.totalInput != null
? parts.totalInput - parts.cacheRead - parts.cacheWrite
: undefined);
const textOutput =
parts.textOutput ??
(parts.totalOutput != null
? parts.totalOutput - parts.reasoning
: undefined);
return {
inputTokens: clamp(required(textInput, "Input")),
outputTokens: clamp(required(textOutput, "Output")),
cacheReadTokens: clamp(parts.cacheRead),
cacheWriteTokens: clamp(parts.cacheWrite),
reasoningTokens: clamp(parts.reasoning),
};
};

View File

@@ -0,0 +1,144 @@
import { describe, expect, test } from "bun:test";
import type { LanguageModelV3, LanguageModelV3Usage } from "@ai-sdk/provider";
import { generateText, streamText } from "ai";
import { withAutumn } from "../../src/index.js";
type TrackTokensParams = {
customerId: string;
modelId: string;
inputTokens: number;
outputTokens: number;
cacheReadTokens?: number;
cacheWriteTokens?: number;
reasoningTokens?: number;
featureId?: string;
entityId?: string;
properties?: Record<string, unknown>;
};
const usage: LanguageModelV3Usage = {
inputTokens: {
total: 13,
noCache: 10,
cacheRead: 2,
cacheWrite: 1,
},
outputTokens: {
total: 7,
text: 5,
reasoning: 2,
},
};
const finishReason = { unified: "stop" as const, raw: "stop" };
const createAutumn = () => {
const calls: TrackTokensParams[] = [];
return {
calls,
autumn: {
balances: {
trackTokens: async (params: TrackTokensParams) => {
calls.push(params);
},
},
},
};
};
const createModel = (): LanguageModelV3 => ({
specificationVersion: "v3",
provider: "openai",
modelId: "gpt-test",
supportedUrls: {},
async doGenerate() {
return {
content: [{ type: "text", text: "hello" }],
finishReason,
usage,
warnings: [],
};
},
async doStream() {
return {
stream: new ReadableStream({
start(controller) {
controller.enqueue({ type: "text-start", id: "text-1" });
controller.enqueue({
type: "text-delta",
id: "text-1",
delta: "hello",
});
controller.enqueue({ type: "text-end", id: "text-1" });
controller.enqueue({ type: "finish", finishReason, usage });
controller.close();
},
}),
};
},
});
describe("withAutumn", () => {
test("tracks token usage from generateText", async () => {
const { autumn, calls } = createAutumn();
const model = withAutumn({
autumn,
model: createModel(),
customerId: "cus_test",
featureId: "ai_credits",
entityId: "entity_test",
properties: { source: "test" },
});
const result = await generateText({ model, prompt: "Say hello" });
expect(result.text).toBe("hello");
expect(calls).toEqual([
{
customerId: "cus_test",
modelId: "openai/gpt-test",
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 2,
cacheWriteTokens: 1,
reasoningTokens: 2,
featureId: "ai_credits",
entityId: "entity_test",
properties: { source: "test" },
},
]);
});
test("tracks token usage from streamText when the stream finishes", async () => {
const { autumn, calls } = createAutumn();
const model = withAutumn({
autumn,
model: createModel(),
customerId: "cus_stream",
providerId: "custom-openai",
});
const result = streamText({ model, prompt: "Say hello" });
const chunks: string[] = [];
for await (const chunk of result.textStream) {
chunks.push(chunk);
}
expect(chunks.join("")).toBe("hello");
expect(calls).toEqual([
{
customerId: "cus_stream",
modelId: "custom-openai/gpt-test",
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 2,
cacheWriteTokens: 1,
reasoningTokens: 2,
},
]);
});
});

View File

@@ -0,0 +1,93 @@
import { describe, expect, test } from "bun:test";
import { normalizeUsage } from "../../src/usage.js";
const MODEL = "openai/gpt-test";
describe("normalizeUsage", () => {
test("nested V3 counts split into exclusive pools", () => {
expect(
normalizeUsage(
{
inputTokens: { total: 13, noCache: 10, cacheRead: 2, cacheWrite: 1 },
outputTokens: { total: 7, text: 5, reasoning: 2 },
},
MODEL,
),
).toEqual({
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 2,
cacheWriteTokens: 1,
reasoningTokens: 2,
});
});
test("nested totals without breakdowns derive text pools", () => {
expect(
normalizeUsage(
{
inputTokens: { total: 13, cacheRead: 2, cacheWrite: 1 },
outputTokens: { total: 7, reasoning: 2 },
},
MODEL,
),
).toEqual({
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 2,
cacheWriteTokens: 1,
reasoningTokens: 2,
});
});
test("flat counts with token details", () => {
expect(
normalizeUsage(
{
inputTokens: 13,
outputTokens: 7,
inputTokenDetails: { cacheReadTokens: 2, cacheWriteTokens: 1 },
outputTokenDetails: { reasoningTokens: 2 },
},
MODEL,
),
).toEqual({
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 2,
cacheWriteTokens: 1,
reasoningTokens: 2,
});
});
test("legacy prompt/completion counts", () => {
expect(
normalizeUsage(
{ promptTokens: 100, completionTokens: { total: 50 } },
MODEL,
),
).toEqual({
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
});
});
test("inconsistent totals clamp to zero instead of going negative", () => {
const pools = normalizeUsage(
{
inputTokens: { total: 1, cacheRead: 5, cacheWrite: 0 },
outputTokens: { total: 1, reasoning: 5 },
},
MODEL,
);
expect(pools.inputTokens).toBe(0);
expect(pools.outputTokens).toBe(0);
});
test("missing usage throws with the model name", () => {
expect(() => normalizeUsage({}, MODEL)).toThrow(/gpt-test/);
});
});

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"strictNullChecks": true,
"target": "ES2022",
"moduleResolution": "NodeNext",
"module": "NodeNext",
"declaration": true,
"isolatedModules": true,
"noEmit": true,
"outDir": "dist",
"lib": ["ES2022"],
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: {
"sdk/index": "src/index.ts",
},
format: ["cjs", "esm"],
dts: true,
splitting: false,
sourcemap: false,
clean: true,
});

View File

@@ -349,6 +349,32 @@ function normalizeFeatureForCompare(f: Feature): Record<string, unknown> {
}));
}
if (f.type === "ai_credit_system") {
const ai = f as Extract<Feature, { type: "ai_credit_system" }>;
if (ai.modelMarkups && Object.keys(ai.modelMarkups).length > 0) {
result.modelMarkups = Object.fromEntries(
Object.entries(ai.modelMarkups)
.sort(([a], [b]) => a.localeCompare(b))
.map(([modelId, entry]) => [
modelId,
{
markup: entry.markup,
inputCost: entry.inputCost,
outputCost: entry.outputCost,
},
]),
);
}
if (ai.defaultMarkup != null) result.defaultMarkup = ai.defaultMarkup;
if (ai.providerMarkups && Object.keys(ai.providerMarkups).length > 0) {
result.providerMarkups = Object.fromEntries(
Object.entries(ai.providerMarkups).sort(([a], [b]) =>
a.localeCompare(b),
),
);
}
}
return result;
}

View File

@@ -4,92 +4,108 @@
import { z } from "zod/v4";
export const FeatureSchema = z.object({
id: z.string().meta({
description:
"The unique identifier for this feature, used in /check and /track calls.",
}),
name: z.string().meta({
description:
"Human-readable name displayed in the dashboard and billing UI.",
}),
eventNames: z.array(z.string()).optional().meta({
description:
"Event names that trigger this feature's balance. Allows multiple features to respond to a single event.",
}),
creditSchema: z
.array(
z.object({
metered_feature_id: z.string().meta({
description:
"ID of the metered feature that draws from this credit system.",
}),
credit_cost: z.number().meta({
description: "Credits consumed per unit of the metered feature.",
}),
}),
)
.optional()
.meta({
description:
"For credit_system features: maps metered features to their credit costs.",
}),
archived: z.boolean().meta({
description:
"Whether the feature is archived and hidden from the dashboard.",
})
id: z.string().meta({
description:
"The unique identifier for this feature, used in /check and /track calls.",
}),
name: z.string().meta({
description:
"Human-readable name displayed in the dashboard and billing UI.",
}),
eventNames: z.array(z.string()).optional().meta({
description:
"Event names that trigger this feature's balance. Allows multiple features to respond to a single event.",
}),
creditSchema: z
.array(
z.object({
metered_feature_id: z.string().meta({
description:
"ID of the metered feature that draws from this credit system.",
}),
credit_cost: z.number().meta({
description: "Credits consumed per unit of the metered feature.",
}),
}),
)
.optional()
.meta({
description:
"For credit_system features: maps metered features to their credit costs.",
}),
archived: z.boolean().meta({
description:
"Whether the feature is archived and hidden from the dashboard.",
}),
});
// Base fields shared by all feature types
type FeatureBase = {
/** Unique identifier for the feature */
id: string;
/** Display name for the feature */
name: string;
/** Whether the feature is archived */
archived?: boolean;
/** Event names that trigger this feature */
eventNames?: string[];
/** Credit schema for credit_system features */
creditSchema?: Array<{
meteredFeatureId: string;
creditCost: number;
}>;
/** Unique identifier for the feature */
id: string;
/** Display name for the feature */
name: string;
/** Whether the feature is archived */
archived?: boolean;
/** Event names that trigger this feature */
eventNames?: string[];
/** Credit schema for credit_system features */
creditSchema?: Array<{
meteredFeatureId: string;
creditCost: number;
}>;
};
/** Boolean feature - no consumable field allowed */
export type BooleanFeature = FeatureBase & {
type: "boolean";
consumable?: never;
type: "boolean";
consumable?: never;
};
/** Metered feature - requires consumable field */
export type MeteredFeature = FeatureBase & {
type: "metered";
/** Whether usage is consumed (true) or accumulated (false) */
consumable: boolean;
type: "metered";
/** Whether usage is consumed (true) or accumulated (false) */
consumable: boolean;
};
/** Credit system feature - always consumable */
export type CreditSystemFeature = FeatureBase & {
type: "credit_system";
/** Credit systems are always consumable */
consumable?: true;
/** Required: defines how credits map to metered features */
creditSchema: Array<{
meteredFeatureId: string;
creditCost: number;
}>;
type: "credit_system";
/** Credit systems are always consumable */
consumable?: true;
/** Required: defines how credits map to metered features */
creditSchema: Array<{
meteredFeatureId: string;
creditCost: number;
}>;
};
/**
* Feature definition with type-safe constraints:
* - Boolean features cannot have consumable
* - Metered features require consumable (true = single_use style, false = continuous_use style)
* - Credit system features are always consumable and require creditSchema
*/
export type Feature = BooleanFeature | MeteredFeature | CreditSystemFeature;
export type ModelMarkupEntry = {
/** Per-model markup override. Omit to inherit provider/global markup. */
markup?: number;
inputCost?: number;
outputCost?: number;
};
export type ProviderMarkupEntry = {
markup: number;
};
/** AI credit system feature - uses model-based pricing */
export type AiCreditSystemFeature = FeatureBase & {
type: "ai_credit_system";
/** Per-model markup overrides (highest priority). */
modelMarkups?: Record<string, ModelMarkupEntry>;
/** Default markup applied when no model or provider markup matches. */
defaultMarkup?: number;
/** Per-provider default markups, keyed by the first segment of the model id. */
providerMarkups?: Record<string, ProviderMarkupEntry>;
};
export type Feature =
| BooleanFeature
| MeteredFeature
| CreditSystemFeature
| AiCreditSystemFeature;

View File

@@ -6,14 +6,14 @@ import { createTransformer } from "./Transformer.js";
describe("Transformer", () => {
describe("Feature transforms", () => {
test("boolean feature", () => {
const apiFeature = {
const result = transformApiFeature({
id: "enabled",
name: "Feature Enabled",
type: "boolean",
consumable: false,
archived: false,
event_names: [],
};
const result = transformApiFeature(apiFeature);
});
expect(result.type).toBe("boolean");
expect(result.id).toBe("enabled");
@@ -21,47 +21,72 @@ describe("Transformer", () => {
});
test("single_use → metered with consumable=true", () => {
const apiFeature = {
const result = transformApiFeature({
id: "api_calls",
name: "API Calls",
type: "single_use",
consumable: true,
archived: false,
event_names: ["api.call"],
};
const result = transformApiFeature(apiFeature);
});
expect(result.type).toBe("metered");
expect(result.consumable).toBe(true);
if (result.type === "metered") {
expect(result.consumable).toBe(true);
}
expect(result.id).toBe("api_calls");
});
test("continuous_use → metered with consumable=false", () => {
const apiFeature = {
const result = transformApiFeature({
id: "seats",
name: "Seats",
type: "continuous_use",
consumable: false,
archived: false,
event_names: [],
};
const result = transformApiFeature(apiFeature);
});
expect(result.type).toBe("metered");
expect(result.consumable).toBe(false);
if (result.type === "metered") {
expect(result.consumable).toBe(false);
}
});
test("credit_system", () => {
const apiFeature = {
const result = transformApiFeature({
id: "credits",
name: "Credits",
type: "credit_system",
consumable: true,
archived: false,
credit_schema: [{ metered_feature_id: "api_calls", credit_cost: 10 }],
};
const result = transformApiFeature(apiFeature);
});
expect(result.type).toBe("credit_system");
expect(result.consumable).toBe(true);
expect(result.creditSchema).toHaveLength(1);
if (result.type === "credit_system") {
expect(result.consumable).toBe(true);
expect(result.creditSchema).toHaveLength(1);
}
});
test("ai_credit_system", () => {
const result = transformApiFeature({
id: "ai_credits",
name: "AI Credits",
type: "ai_credit_system",
consumable: true,
archived: false,
model_markups: {
"anthropic/claude-opus-4-5": { markup: 20 },
},
});
expect(result.type).toBe("ai_credit_system");
if (result.type === "ai_credit_system") {
expect(result.modelMarkups).toBeDefined();
expect(result.modelMarkups!["anthropic/claude-opus-4-5"].markup).toBe(20);
}
});
});

View File

@@ -1,26 +1,38 @@
import type { Feature } from "../../../compose/models/featureModels.js";
import type { Feature, ModelMarkupEntry } from "../../../compose/models/featureModels.js";
import type { ApiFeature } from "../../api/types/feature.js";
import { createTransformer } from "./Transformer.js";
type RawApiFeature = Omit<ApiFeature, "type"> & { type: string };
function mapCreditSchema(
api: any,
api: RawApiFeature,
): Array<{ meteredFeatureId: string; creditCost: number }> {
return (api.credit_schema ?? []).map(
(cs: { metered_feature_id: string; credit_cost: number }) => ({
meteredFeatureId: cs.metered_feature_id,
creditCost: cs.credit_cost,
}),
return (api.credit_schema ?? []).map((cs) => ({
meteredFeatureId: cs.metered_feature_id,
creditCost: cs.credit_cost,
}));
}
function mapModelMarkups(api: RawApiFeature): Record<string, ModelMarkupEntry> | undefined {
if (!api.model_markups) return undefined;
return Object.fromEntries(
Object.entries(api.model_markups).map(([modelId, entry]) => [
modelId,
{
markup: entry.markup,
inputCost: entry.input_cost,
outputCost: entry.output_cost,
},
])
);
}
const BASE_COMPUTE = {
eventNames: (api: any) =>
eventNames: (api: RawApiFeature) =>
api.event_names && api.event_names.length > 0 ? api.event_names : undefined,
};
/**
* Declarative feature transformer - replaces 79 lines with 40 lines of config
*/
export const featureTransformer = createTransformer<any, Feature>({
export const featureTransformer = createTransformer<RawApiFeature, Feature>({
discriminator: "type",
cases: {
// Boolean features: just copy base fields, no consumable
@@ -32,14 +44,24 @@ export const featureTransformer = createTransformer<any, Feature>({
},
},
// Credit system features: always consumable
credit_system: {
copy: ["id", "name", "archived"],
compute: {
...BASE_COMPUTE,
type: () => "credit_system" as const,
consumable: () => true,
creditSchema: mapCreditSchema,
creditSchema: (api) => mapCreditSchema(api),
},
},
ai_credit_system: {
copy: ["id", "name", "archived"],
compute: {
...BASE_COMPUTE,
type: () => "ai_credit_system" as const,
modelMarkups: (api) => mapModelMarkups(api),
defaultMarkup: (api) => api.default_markup ?? undefined,
providerMarkups: (api) => api.provider_markups ?? undefined,
},
},
@@ -85,6 +107,6 @@ export const featureTransformer = createTransformer<any, Feature>({
},
});
export function transformApiFeature(apiFeature: any): Feature {
export function transformApiFeature(apiFeature: RawApiFeature): Feature {
return featureTransformer.transform(apiFeature);
}

Some files were not shown because too many files have changed in this diff Show More