Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project are documented here. The format follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html): only a major version changes the
public API, a minor version adds to it, and a patch version fixes it.

## [Unreleased]

### Added

- `transaction_id` on `instances.report_usage()`: the API applies a keyed report at most once,
so a keyed report is retried like a read, every attempt under the same key; a report without a
key is still sent once. A malformed key raises `ValueError` before any request.
- `TransactionIdReusedError`, a `ConflictError` raised when a key was already used for a
different report; never retried. A conflict over the threshold stays `ThresholdExceededError`.
- `instances.report_usage_detailed()` returns a `UsageReportResult` with the usage, `replayed`
and `metadata_dropped`.
- Against an API without `transaction_id`, the report is resent without it and the client stops
sending keys for its lifetime, logging a warning once.
- `instances.list_usage_reports()`, `instances.export_usage_reports()` and
`instances.export_organization_usage_reports()`: the usage history, and CSV/NDJSON exports
streamed into a binary destination.

## [1.0.0] - 2026-10-01

The first release of the Kaiten Python SDK, generated and tested against the contract of
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,50 @@ except ThresholdExceededError as error:
`behavior="append"` (the default) folds the value into the total through the entitlement's
aggregation method; `behavior="set"` replaces the total, which is also how to correct it.

A report without a key is sent once and never retried: if its response is lost, it may or may
not have been counted. Give it a `transaction_id` and the API applies it at most once (per
instance, entitlement and key, within 35 days by default), so the SDK retries it like a read,
every attempt under the same key:

```python
from kaitencloud import TransactionIdReusedError

try:
result = client.instances.report_usage_detailed(
"acme-production",
"tokens",
1200,
transaction_id="llm-call:9f2c:tokens", # a UUID, or the business event id plus the meter
)
except TransactionIdReusedError as error:
# The key was already used for a different report: a bug in how keys are made, never
# retried. A correction is a new report under a new key.
print("Original report:", error.errors[0].value)
else:
print(result.usage.value, result.replayed, result.metadata_dropped)
```

`result.replayed` says the API had already counted this report and `result.usage` is its
original answer; `result.metadata_dropped` that the metadata was above 4 KiB and not stored. An
API older than `transaction_id` refuses the field: the report is then resent without it, and the
client stops sending keys for its lifetime, logging a warning once.

The usage history lists every accepted report, with the counter before and after it and the
limit it was gated on. Exports stream into a binary file as they arrive:

```python
reports = client.instances.list_usage_reports("acme-production", "tokens", from_="2026-10-01T00:00:00Z")

with open("tokens.csv", "wb") as destination:
client.instances.export_usage_reports("acme-production", "tokens", destination)

# The whole organization, 31 days at a time; instance_id reaches a deleted instance.
with open("september.ndjson", "wb") as destination:
client.instances.export_organization_usage_reports(
destination, from_="2026-09-01T00:00:00Z", to="2026-10-01T00:00:00Z", format="json"
)
```

`kaitencloud.usage` computes the same boundaries the server enforces, to render a meter or check
a report before sending it:

Expand Down Expand Up @@ -665,6 +709,10 @@ all 10 of its operations. The async clients have the same methods.
| `client.instances.list_usage()` | `GET /instances/{instanceSlug}/entitlements/usage` |
| `client.instances.get_usage()` | `GET /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage` |
| `client.instances.report_usage()` | `POST /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage` |
| `client.instances.report_usage_detailed()` | `POST /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage` |
| `client.instances.list_usage_reports()` | `GET /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage/reports` |
| `client.instances.export_usage_reports()` | `GET /instances/{instanceSlug}/entitlements/{entitlementSlug}/usage/reports/export` |
| `client.instances.export_organization_usage_reports()` | `GET /usage/reports/export` |
| `client.instances.get_integration()` | `GET /instances/{instanceSlug}/integrations/{integrationName}` |
| `client.instances.create_integration()` | `POST /instances/{instanceSlug}/integrations/{integrationName}` |
| `client.instances.update_integration()` | `PUT /instances/{instanceSlug}/integrations/{integrationName}` |
Expand Down
5 changes: 4 additions & 1 deletion openapi/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ core:
getAuditTrails: instances.list_audit_trails
getEntitlementsUsageMetrics: instances.list_usage
getEntitlementUsageMetrics: instances.get_usage
reportEntitlementUsageMetric: instances.report_usage
reportEntitlementUsageMetric: [instances.report_usage, instances.report_usage_detailed]
listUsageReports: instances.list_usage_reports
exportUsageReports: instances.export_usage_reports
exportOrganizationUsageReports: instances.export_organization_usage_reports
get-instance-integration: instances.get_integration
create-instance-integration: instances.create_integration
update-instance-integration: instances.update_integration
Expand Down
Loading
Loading