From 209d9b852cee0478d836e0679dd7ed2f7d81d6a2 Mon Sep 17 00:00:00 2001 From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:32:03 +0000 Subject: [PATCH 1/3] [fern-generated] Update SDK Generated by Fern CLI Version: unknown Generators: - fernapi/fern-python-sdk: 5.12.12 --- .fern/metadata.json | 6 +- reference.md | 415 ++++++++++ src/smallestai/atoms/__init__.py | 53 ++ src/smallestai/atoms/billing/__init__.py | 37 + src/smallestai/atoms/billing/client.py | 518 ++++++++++++ src/smallestai/atoms/billing/raw_client.py | 747 ++++++++++++++++++ .../atoms/billing/types/__init__.py | 38 + .../types/get_ledger_billing_request_scope.py | 5 + .../types/get_ledger_billing_request_type.py | 17 + ...get_calls_response_data_transcript_item.py | 4 +- src/smallestai/atoms/client.py | 19 + src/smallestai/atoms/types/__init__.py | 45 ++ .../atoms/types/bad_request_error_body.py | 5 +- .../types/bad_request_error_body_error.py | 21 + .../atoms/types/billing_balance_response.py | 22 + .../types/billing_balance_response_data.py | 41 + .../types/billing_invoice_list_response.py | 24 + .../types/billing_invoice_pdf_response.py | 22 + .../billing_invoice_pdf_response_data.py | 29 + .../atoms/types/billing_ledger_entry.py | 73 ++ .../billing_ledger_entry_transaction_type.py | 16 + .../atoms/types/billing_ledger_response.py | 22 + .../types/billing_ledger_response_data.py | 37 + .../billing_ledger_response_data_period.py | 34 + .../billing_ledger_response_data_summary.py | 36 + .../types/billing_usage_breakdown_response.py | 22 + .../billing_usage_breakdown_response_data.py | 41 + .../atoms/types/not_found_error_body.py | 5 +- .../atoms/types/not_found_error_body_error.py | 21 + src/smallestai/environment.py | 8 +- tests/wire/conftest.py | 4 +- tests/wire/test_atoms_billing.py | 50 ++ wiremock/wiremock-mappings.json | 174 +++- 33 files changed, 2597 insertions(+), 14 deletions(-) create mode 100644 src/smallestai/atoms/billing/__init__.py create mode 100644 src/smallestai/atoms/billing/client.py create mode 100644 src/smallestai/atoms/billing/raw_client.py create mode 100644 src/smallestai/atoms/billing/types/__init__.py create mode 100644 src/smallestai/atoms/billing/types/get_ledger_billing_request_scope.py create mode 100644 src/smallestai/atoms/billing/types/get_ledger_billing_request_type.py create mode 100644 src/smallestai/atoms/types/bad_request_error_body_error.py create mode 100644 src/smallestai/atoms/types/billing_balance_response.py create mode 100644 src/smallestai/atoms/types/billing_balance_response_data.py create mode 100644 src/smallestai/atoms/types/billing_invoice_list_response.py create mode 100644 src/smallestai/atoms/types/billing_invoice_pdf_response.py create mode 100644 src/smallestai/atoms/types/billing_invoice_pdf_response_data.py create mode 100644 src/smallestai/atoms/types/billing_ledger_entry.py create mode 100644 src/smallestai/atoms/types/billing_ledger_entry_transaction_type.py create mode 100644 src/smallestai/atoms/types/billing_ledger_response.py create mode 100644 src/smallestai/atoms/types/billing_ledger_response_data.py create mode 100644 src/smallestai/atoms/types/billing_ledger_response_data_period.py create mode 100644 src/smallestai/atoms/types/billing_ledger_response_data_summary.py create mode 100644 src/smallestai/atoms/types/billing_usage_breakdown_response.py create mode 100644 src/smallestai/atoms/types/billing_usage_breakdown_response_data.py create mode 100644 src/smallestai/atoms/types/not_found_error_body_error.py create mode 100644 tests/wire/test_atoms_billing.py diff --git a/.fern/metadata.json b/.fern/metadata.json index 105761d3..bbf2558d 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -45,10 +45,10 @@ } ] }, - "originGitCommit": "c3e8467d7de3ac4b4d54e3c9bc4828bebfc4c792", + "originGitCommit": "e9ca686b515042154390e137de05194b4a191566", "originGitCommitIsDirty": false, "invokedBy": "ci", - "requestedVersion": "5.3.0", + "requestedVersion": "5.3.1", "ciProvider": "github", - "sdkVersion": "5.3.0" + "sdkVersion": "5.3.1" } \ No newline at end of file diff --git a/reference.md b/reference.md index 2e8e9097..9b91ef71 100644 --- a/reference.md +++ b/reference.md @@ -13050,6 +13050,421 @@ the caller's org. + + + + +## Atoms Billing +
client.atoms.billing.get_balance() -> BillingBalanceResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns the organization's current credit balance in USD plus the +current plan identifier. Organization is resolved from the API key, +so no `X-Organization-Id` header is required. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from smallestai import SmallestAI +from smallestai.environment import SmallestAIEnvironment + +client = SmallestAI( + api_key="", + environment=SmallestAIEnvironment.PRODUCTION, +) + +client.atoms.billing.get_balance() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.atoms.billing.get_ledger(...) -> BillingLedgerResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Paginated credit-ledger transaction history. Ledger reads are served +from ClickHouse; a rare storage-tier outage returns an empty +`transactions` array rather than a 5xx. + +**Window rules** + +- `from` defaults to `to - 7 days`, `to` defaults to now. +- The span between `from` and `to` cannot exceed **90 days**. A wider + span returns 400. Page through longer periods by making multiple + calls with shifted `from`/`to`. +- `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older + historical data is not available via API; contact support for bulk + exports. +- `from > to` returns 400. + +**Filters** + +- `type` filters to a single transaction type. `PAYMENTS` is a + virtual filter that returns both `CREDIT_PURCHASE` and + `AUTO_RELOAD` rows. +- `scope` filters `USAGE_DEDUCTION` rows by product category. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from smallestai import SmallestAI +from smallestai.environment import SmallestAIEnvironment +import datetime + +client = SmallestAI( + api_key="", + environment=SmallestAIEnvironment.PRODUCTION, +) + +client.atoms.billing.get_ledger( + from_=datetime.datetime.fromisoformat("2026-07-01T00:00:00+00:00"), + to=datetime.datetime.fromisoformat("2026-07-28T00:00:00+00:00"), +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size (1–100). + +
+
+ +
+
+ +**offset:** `typing.Optional[int]` — Offset for pagination. + +
+
+ +
+
+ +**from:** `typing.Optional[datetime.datetime]` — Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`. + +
+
+ +
+
+ +**to:** `typing.Optional[datetime.datetime]` — Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now. + +
+
+ +
+
+ +**type:** `typing.Optional[GetLedgerBillingRequestType]` — Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`). + +
+
+ +
+
+ +**scope:** `typing.Optional[GetLedgerBillingRequestScope]` — Filter by spend category. Applies only to `USAGE_DEDUCTION` rows. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.atoms.billing.get_usage_breakdown() -> BillingUsageBreakdownResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Total credits spent so far, split across the three product scopes. +The window is a cumulative snapshot from **2026-03-02T00:00:00Z** +(the platform's usage-tracking start date) up to the current +instant. Served from ClickHouse. No query parameters. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from smallestai import SmallestAI +from smallestai.environment import SmallestAIEnvironment + +client = SmallestAI( + api_key="", + environment=SmallestAIEnvironment.PRODUCTION, +) + +client.atoms.billing.get_usage_breakdown() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.atoms.billing.list_invoices() -> BillingInvoiceListResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns up to 20 of the most recent Stripe invoices for the caller's +organization. Each item is the raw Stripe `Invoice` object; use the +canonical Stripe reference at +[stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object) +for field-level semantics. + +Organizations that have never been charged (free-tier only) return +an empty array. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from smallestai import SmallestAI +from smallestai.environment import SmallestAIEnvironment + +client = SmallestAI( + api_key="", + environment=SmallestAIEnvironment.PRODUCTION, +) + +client.atoms.billing.list_invoices() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.atoms.billing.get_invoice_pdf(...) -> BillingInvoicePdfResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns a Stripe-hosted PDF URL for the invoice. The URL is +short-lived; fetch fresh when you need to hand it to a user. + +Requesting an invoice that does not belong to the caller's +organization returns 404 (not 403), so a foreign invoice ID cannot +be confirmed to exist. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from smallestai import SmallestAI +from smallestai.environment import SmallestAIEnvironment + +client = SmallestAI( + api_key="", + environment=SmallestAIEnvironment.PRODUCTION, +) + +client.atoms.billing.get_invoice_pdf( + invoice_id="invoiceId", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**invoice_id:** `str` — Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`). + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/src/smallestai/atoms/__init__.py b/src/smallestai/atoms/__init__.py index c812c10b..f519704a 100644 --- a/src/smallestai/atoms/__init__.py +++ b/src/smallestai/atoms/__init__.py @@ -54,7 +54,21 @@ AnalyticsTrendMetric, ApiResponse, BadRequestErrorBody, + BadRequestErrorBodyError, BadRequestErrorResponse, + BillingBalanceResponse, + BillingBalanceResponseData, + BillingInvoiceListResponse, + BillingInvoicePdfResponse, + BillingInvoicePdfResponseData, + BillingLedgerEntry, + BillingLedgerEntryTransactionType, + BillingLedgerResponse, + BillingLedgerResponseData, + BillingLedgerResponseDataPeriod, + BillingLedgerResponseDataSummary, + BillingUsageBreakdownResponse, + BillingUsageBreakdownResponseData, Branch, BranchStatus, BranchSummary, @@ -93,6 +107,7 @@ LockedErrorResponse, LockedErrorResponseErrorType, NotFoundErrorBody, + NotFoundErrorBodyError, PostCallAnalyticsConfig, PostCallAnalyticsConfigSuccessMetricsItem, PostCallAnalyticsConfigSuccessMetricsItemSuccessMetricType, @@ -199,6 +214,7 @@ agents, analytics, audience, + billing, call_actions, calls, campaigns, @@ -429,6 +445,7 @@ PostAudienceResponse, PostAudienceResponseData, ) + from .billing import GetLedgerBillingRequestScope, GetLedgerBillingRequestType from .call_actions import ( CreateCallActionRequestActionType, CreateCallActionRequestCategory, @@ -705,7 +722,21 @@ "BadGatewayError": ".errors", "BadRequestError": ".errors", "BadRequestErrorBody": ".types", + "BadRequestErrorBodyError": ".types", "BadRequestErrorResponse": ".types", + "BillingBalanceResponse": ".types", + "BillingBalanceResponseData": ".types", + "BillingInvoiceListResponse": ".types", + "BillingInvoicePdfResponse": ".types", + "BillingInvoicePdfResponseData": ".types", + "BillingLedgerEntry": ".types", + "BillingLedgerEntryTransactionType": ".types", + "BillingLedgerResponse": ".types", + "BillingLedgerResponseData": ".types", + "BillingLedgerResponseDataPeriod": ".types", + "BillingLedgerResponseDataSummary": ".types", + "BillingUsageBreakdownResponse": ".types", + "BillingUsageBreakdownResponseData": ".types", "Branch": ".types", "BranchStatus": ".types", "BranchSummary": ".types", @@ -922,6 +953,8 @@ "GetKnowledgebaseIdItemsResponse": ".knowledge_base", "GetKnowledgebaseIdScrapedUrlsResponse": ".knowledge_base", "GetKnowledgebaseIdScrapedUrlsResponseDataItem": ".knowledge_base", + "GetLedgerBillingRequestScope": ".billing", + "GetLedgerBillingRequestType": ".billing", "GetOrganizationResponse": ".organization", "GetOrganizationResponseData": ".organization", "GetOrganizationResponseDataMembersItem": ".organization", @@ -1039,6 +1072,7 @@ "ModifyWebEngageIntegrationResponse": ".integrations", "NotFoundError": ".errors", "NotFoundErrorBody": ".types", + "NotFoundErrorBodyError": ".types", "PauseCampaignsResponse": ".campaigns", "PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem": ".webhooks", "PostAgentAgentIdWebhookSubscriptionsResponse": ".webhooks", @@ -1226,6 +1260,7 @@ "agents": ".agents", "analytics": ".analytics", "audience": ".audience", + "billing": ".billing", "call_actions": ".call_actions", "calls": ".calls", "campaigns": ".campaigns", @@ -1323,7 +1358,21 @@ def __dir__(): "BadGatewayError", "BadRequestError", "BadRequestErrorBody", + "BadRequestErrorBodyError", "BadRequestErrorResponse", + "BillingBalanceResponse", + "BillingBalanceResponseData", + "BillingInvoiceListResponse", + "BillingInvoicePdfResponse", + "BillingInvoicePdfResponseData", + "BillingLedgerEntry", + "BillingLedgerEntryTransactionType", + "BillingLedgerResponse", + "BillingLedgerResponseData", + "BillingLedgerResponseDataPeriod", + "BillingLedgerResponseDataSummary", + "BillingUsageBreakdownResponse", + "BillingUsageBreakdownResponseData", "Branch", "BranchStatus", "BranchSummary", @@ -1540,6 +1589,8 @@ def __dir__(): "GetKnowledgebaseIdItemsResponse", "GetKnowledgebaseIdScrapedUrlsResponse", "GetKnowledgebaseIdScrapedUrlsResponseDataItem", + "GetLedgerBillingRequestScope", + "GetLedgerBillingRequestType", "GetOrganizationResponse", "GetOrganizationResponseData", "GetOrganizationResponseDataMembersItem", @@ -1657,6 +1708,7 @@ def __dir__(): "ModifyWebEngageIntegrationResponse", "NotFoundError", "NotFoundErrorBody", + "NotFoundErrorBodyError", "PauseCampaignsResponse", "PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem", "PostAgentAgentIdWebhookSubscriptionsResponse", @@ -1844,6 +1896,7 @@ def __dir__(): "agents", "analytics", "audience", + "billing", "call_actions", "calls", "campaigns", diff --git a/src/smallestai/atoms/billing/__init__.py b/src/smallestai/atoms/billing/__init__.py new file mode 100644 index 00000000..c7898fd6 --- /dev/null +++ b/src/smallestai/atoms/billing/__init__.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import GetLedgerBillingRequestScope, GetLedgerBillingRequestType +_dynamic_imports: typing.Dict[str, str] = { + "GetLedgerBillingRequestScope": ".types", + "GetLedgerBillingRequestType": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["GetLedgerBillingRequestScope", "GetLedgerBillingRequestType"] diff --git a/src/smallestai/atoms/billing/client.py b/src/smallestai/atoms/billing/client.py new file mode 100644 index 00000000..eebcce73 --- /dev/null +++ b/src/smallestai/atoms/billing/client.py @@ -0,0 +1,518 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ..types.billing_balance_response import BillingBalanceResponse +from ..types.billing_invoice_list_response import BillingInvoiceListResponse +from ..types.billing_invoice_pdf_response import BillingInvoicePdfResponse +from ..types.billing_ledger_response import BillingLedgerResponse +from ..types.billing_usage_breakdown_response import BillingUsageBreakdownResponse +from .raw_client import AsyncRawBillingClient, RawBillingClient +from .types.get_ledger_billing_request_scope import GetLedgerBillingRequestScope +from .types.get_ledger_billing_request_type import GetLedgerBillingRequestType + + +class BillingClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawBillingClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawBillingClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawBillingClient + """ + return self._raw_client + + def get_balance(self, *, request_options: typing.Optional[RequestOptions] = None) -> BillingBalanceResponse: + """ + Returns the organization's current credit balance in USD plus the + current plan identifier. Organization is resolved from the API key, + so no `X-Organization-Id` header is required. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingBalanceResponse + Current balance for the caller's organization. + + Examples + -------- + from smallestai import SmallestAI + + client = SmallestAI( + api_key="YOUR_API_KEY", + ) + client.atoms.billing.get_balance() + """ + _response = self._raw_client.get_balance(request_options=request_options) + return _response.data + + def get_ledger( + self, + *, + limit: typing.Optional[int] = None, + offset: typing.Optional[int] = None, + from_: typing.Optional[dt.datetime] = None, + to: typing.Optional[dt.datetime] = None, + type: typing.Optional[GetLedgerBillingRequestType] = None, + scope: typing.Optional[GetLedgerBillingRequestScope] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> BillingLedgerResponse: + """ + Paginated credit-ledger transaction history. Ledger reads are served + from ClickHouse; a rare storage-tier outage returns an empty + `transactions` array rather than a 5xx. + + **Window rules** + + - `from` defaults to `to - 7 days`, `to` defaults to now. + - The span between `from` and `to` cannot exceed **90 days**. A wider + span returns 400. Page through longer periods by making multiple + calls with shifted `from`/`to`. + - `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older + historical data is not available via API; contact support for bulk + exports. + - `from > to` returns 400. + + **Filters** + + - `type` filters to a single transaction type. `PAYMENTS` is a + virtual filter that returns both `CREDIT_PURCHASE` and + `AUTO_RELOAD` rows. + - `scope` filters `USAGE_DEDUCTION` rows by product category. + + Parameters + ---------- + limit : typing.Optional[int] + Page size (1–100). + + offset : typing.Optional[int] + Offset for pagination. + + from_ : typing.Optional[dt.datetime] + Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`. + + to : typing.Optional[dt.datetime] + Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now. + + type : typing.Optional[GetLedgerBillingRequestType] + Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`). + + scope : typing.Optional[GetLedgerBillingRequestScope] + Filter by spend category. Applies only to `USAGE_DEDUCTION` rows. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingLedgerResponse + Paginated ledger with summary + query window echo. + + Examples + -------- + import datetime + + from smallestai import SmallestAI + + client = SmallestAI( + api_key="YOUR_API_KEY", + ) + client.atoms.billing.get_ledger( + from_=datetime.datetime.fromisoformat( + "2026-07-01 00:00:00+00:00", + ), + to=datetime.datetime.fromisoformat( + "2026-07-28 00:00:00+00:00", + ), + ) + """ + _response = self._raw_client.get_ledger( + limit=limit, offset=offset, from_=from_, to=to, type=type, scope=scope, request_options=request_options + ) + return _response.data + + def get_usage_breakdown( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> BillingUsageBreakdownResponse: + """ + Total credits spent so far, split across the three product scopes. + The window is a cumulative snapshot from **2026-03-02T00:00:00Z** + (the platform's usage-tracking start date) up to the current + instant. Served from ClickHouse. No query parameters. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingUsageBreakdownResponse + Spend split by scope. + + Examples + -------- + from smallestai import SmallestAI + + client = SmallestAI( + api_key="YOUR_API_KEY", + ) + client.atoms.billing.get_usage_breakdown() + """ + _response = self._raw_client.get_usage_breakdown(request_options=request_options) + return _response.data + + def list_invoices(self, *, request_options: typing.Optional[RequestOptions] = None) -> BillingInvoiceListResponse: + """ + Returns up to 20 of the most recent Stripe invoices for the caller's + organization. Each item is the raw Stripe `Invoice` object; use the + canonical Stripe reference at + [stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object) + for field-level semantics. + + Organizations that have never been charged (free-tier only) return + an empty array. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingInvoiceListResponse + Up to 20 recent Stripe invoices for the caller's organization. + + Examples + -------- + from smallestai import SmallestAI + + client = SmallestAI( + api_key="YOUR_API_KEY", + ) + client.atoms.billing.list_invoices() + """ + _response = self._raw_client.list_invoices(request_options=request_options) + return _response.data + + def get_invoice_pdf( + self, invoice_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> BillingInvoicePdfResponse: + """ + Returns a Stripe-hosted PDF URL for the invoice. The URL is + short-lived; fetch fresh when you need to hand it to a user. + + Requesting an invoice that does not belong to the caller's + organization returns 404 (not 403), so a foreign invoice ID cannot + be confirmed to exist. + + Parameters + ---------- + invoice_id : str + Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingInvoicePdfResponse + Stripe-hosted PDF URL for the invoice. + + Examples + -------- + from smallestai import SmallestAI + + client = SmallestAI( + api_key="YOUR_API_KEY", + ) + client.atoms.billing.get_invoice_pdf( + invoice_id="invoiceId", + ) + """ + _response = self._raw_client.get_invoice_pdf(invoice_id, request_options=request_options) + return _response.data + + +class AsyncBillingClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawBillingClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawBillingClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawBillingClient + """ + return self._raw_client + + async def get_balance(self, *, request_options: typing.Optional[RequestOptions] = None) -> BillingBalanceResponse: + """ + Returns the organization's current credit balance in USD plus the + current plan identifier. Organization is resolved from the API key, + so no `X-Organization-Id` header is required. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingBalanceResponse + Current balance for the caller's organization. + + Examples + -------- + import asyncio + + from smallestai import AsyncSmallestAI + + client = AsyncSmallestAI( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.atoms.billing.get_balance() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_balance(request_options=request_options) + return _response.data + + async def get_ledger( + self, + *, + limit: typing.Optional[int] = None, + offset: typing.Optional[int] = None, + from_: typing.Optional[dt.datetime] = None, + to: typing.Optional[dt.datetime] = None, + type: typing.Optional[GetLedgerBillingRequestType] = None, + scope: typing.Optional[GetLedgerBillingRequestScope] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> BillingLedgerResponse: + """ + Paginated credit-ledger transaction history. Ledger reads are served + from ClickHouse; a rare storage-tier outage returns an empty + `transactions` array rather than a 5xx. + + **Window rules** + + - `from` defaults to `to - 7 days`, `to` defaults to now. + - The span between `from` and `to` cannot exceed **90 days**. A wider + span returns 400. Page through longer periods by making multiple + calls with shifted `from`/`to`. + - `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older + historical data is not available via API; contact support for bulk + exports. + - `from > to` returns 400. + + **Filters** + + - `type` filters to a single transaction type. `PAYMENTS` is a + virtual filter that returns both `CREDIT_PURCHASE` and + `AUTO_RELOAD` rows. + - `scope` filters `USAGE_DEDUCTION` rows by product category. + + Parameters + ---------- + limit : typing.Optional[int] + Page size (1–100). + + offset : typing.Optional[int] + Offset for pagination. + + from_ : typing.Optional[dt.datetime] + Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`. + + to : typing.Optional[dt.datetime] + Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now. + + type : typing.Optional[GetLedgerBillingRequestType] + Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`). + + scope : typing.Optional[GetLedgerBillingRequestScope] + Filter by spend category. Applies only to `USAGE_DEDUCTION` rows. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingLedgerResponse + Paginated ledger with summary + query window echo. + + Examples + -------- + import asyncio + import datetime + + from smallestai import AsyncSmallestAI + + client = AsyncSmallestAI( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.atoms.billing.get_ledger( + from_=datetime.datetime.fromisoformat( + "2026-07-01 00:00:00+00:00", + ), + to=datetime.datetime.fromisoformat( + "2026-07-28 00:00:00+00:00", + ), + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_ledger( + limit=limit, offset=offset, from_=from_, to=to, type=type, scope=scope, request_options=request_options + ) + return _response.data + + async def get_usage_breakdown( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> BillingUsageBreakdownResponse: + """ + Total credits spent so far, split across the three product scopes. + The window is a cumulative snapshot from **2026-03-02T00:00:00Z** + (the platform's usage-tracking start date) up to the current + instant. Served from ClickHouse. No query parameters. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingUsageBreakdownResponse + Spend split by scope. + + Examples + -------- + import asyncio + + from smallestai import AsyncSmallestAI + + client = AsyncSmallestAI( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.atoms.billing.get_usage_breakdown() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_usage_breakdown(request_options=request_options) + return _response.data + + async def list_invoices( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> BillingInvoiceListResponse: + """ + Returns up to 20 of the most recent Stripe invoices for the caller's + organization. Each item is the raw Stripe `Invoice` object; use the + canonical Stripe reference at + [stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object) + for field-level semantics. + + Organizations that have never been charged (free-tier only) return + an empty array. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingInvoiceListResponse + Up to 20 recent Stripe invoices for the caller's organization. + + Examples + -------- + import asyncio + + from smallestai import AsyncSmallestAI + + client = AsyncSmallestAI( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.atoms.billing.list_invoices() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_invoices(request_options=request_options) + return _response.data + + async def get_invoice_pdf( + self, invoice_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> BillingInvoicePdfResponse: + """ + Returns a Stripe-hosted PDF URL for the invoice. The URL is + short-lived; fetch fresh when you need to hand it to a user. + + Requesting an invoice that does not belong to the caller's + organization returns 404 (not 403), so a foreign invoice ID cannot + be confirmed to exist. + + Parameters + ---------- + invoice_id : str + Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BillingInvoicePdfResponse + Stripe-hosted PDF URL for the invoice. + + Examples + -------- + import asyncio + + from smallestai import AsyncSmallestAI + + client = AsyncSmallestAI( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.atoms.billing.get_invoice_pdf( + invoice_id="invoiceId", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_invoice_pdf(invoice_id, request_options=request_options) + return _response.data diff --git a/src/smallestai/atoms/billing/raw_client.py b/src/smallestai/atoms/billing/raw_client.py new file mode 100644 index 00000000..3a24ec6e --- /dev/null +++ b/src/smallestai/atoms/billing/raw_client.py @@ -0,0 +1,747 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.datetime_utils import serialize_datetime +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import encode_path_param +from ...core.parse_error import ParsingError +from ...core.request_options import RequestOptions +from ...core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.not_found_error import NotFoundError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.billing_balance_response import BillingBalanceResponse +from ..types.billing_invoice_list_response import BillingInvoiceListResponse +from ..types.billing_invoice_pdf_response import BillingInvoicePdfResponse +from ..types.billing_ledger_response import BillingLedgerResponse +from ..types.billing_usage_breakdown_response import BillingUsageBreakdownResponse +from .types.get_ledger_billing_request_scope import GetLedgerBillingRequestScope +from .types.get_ledger_billing_request_type import GetLedgerBillingRequestType +from pydantic import ValidationError + + +class RawBillingClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_balance( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BillingBalanceResponse]: + """ + Returns the organization's current credit balance in USD plus the + current plan identifier. Organization is resolved from the API key, + so no `X-Organization-Id` header is required. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BillingBalanceResponse] + Current balance for the caller's organization. + """ + _response = self._client_wrapper.httpx_client.request( + "payment/v1/credits/balance", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingBalanceResponse, + construct_type( + type_=BillingBalanceResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_ledger( + self, + *, + limit: typing.Optional[int] = None, + offset: typing.Optional[int] = None, + from_: typing.Optional[dt.datetime] = None, + to: typing.Optional[dt.datetime] = None, + type: typing.Optional[GetLedgerBillingRequestType] = None, + scope: typing.Optional[GetLedgerBillingRequestScope] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[BillingLedgerResponse]: + """ + Paginated credit-ledger transaction history. Ledger reads are served + from ClickHouse; a rare storage-tier outage returns an empty + `transactions` array rather than a 5xx. + + **Window rules** + + - `from` defaults to `to - 7 days`, `to` defaults to now. + - The span between `from` and `to` cannot exceed **90 days**. A wider + span returns 400. Page through longer periods by making multiple + calls with shifted `from`/`to`. + - `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older + historical data is not available via API; contact support for bulk + exports. + - `from > to` returns 400. + + **Filters** + + - `type` filters to a single transaction type. `PAYMENTS` is a + virtual filter that returns both `CREDIT_PURCHASE` and + `AUTO_RELOAD` rows. + - `scope` filters `USAGE_DEDUCTION` rows by product category. + + Parameters + ---------- + limit : typing.Optional[int] + Page size (1–100). + + offset : typing.Optional[int] + Offset for pagination. + + from_ : typing.Optional[dt.datetime] + Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`. + + to : typing.Optional[dt.datetime] + Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now. + + type : typing.Optional[GetLedgerBillingRequestType] + Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`). + + scope : typing.Optional[GetLedgerBillingRequestScope] + Filter by spend category. Applies only to `USAGE_DEDUCTION` rows. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BillingLedgerResponse] + Paginated ledger with summary + query window echo. + """ + _response = self._client_wrapper.httpx_client.request( + "payment/v1/credits/ledger", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + params={ + "limit": limit, + "offset": offset, + "from": serialize_datetime(from_) if from_ is not None else None, + "to": serialize_datetime(to) if to is not None else None, + "type": type, + "scope": scope, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingLedgerResponse, + construct_type( + type_=BillingLedgerResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_usage_breakdown( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BillingUsageBreakdownResponse]: + """ + Total credits spent so far, split across the three product scopes. + The window is a cumulative snapshot from **2026-03-02T00:00:00Z** + (the platform's usage-tracking start date) up to the current + instant. Served from ClickHouse. No query parameters. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BillingUsageBreakdownResponse] + Spend split by scope. + """ + _response = self._client_wrapper.httpx_client.request( + "payment/v1/credits/usage/breakdown", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingUsageBreakdownResponse, + construct_type( + type_=BillingUsageBreakdownResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def list_invoices( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BillingInvoiceListResponse]: + """ + Returns up to 20 of the most recent Stripe invoices for the caller's + organization. Each item is the raw Stripe `Invoice` object; use the + canonical Stripe reference at + [stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object) + for field-level semantics. + + Organizations that have never been charged (free-tier only) return + an empty array. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BillingInvoiceListResponse] + Up to 20 recent Stripe invoices for the caller's organization. + """ + _response = self._client_wrapper.httpx_client.request( + "payment/v1/invoices", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingInvoiceListResponse, + construct_type( + type_=BillingInvoiceListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_invoice_pdf( + self, invoice_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BillingInvoicePdfResponse]: + """ + Returns a Stripe-hosted PDF URL for the invoice. The URL is + short-lived; fetch fresh when you need to hand it to a user. + + Requesting an invoice that does not belong to the caller's + organization returns 404 (not 403), so a foreign invoice ID cannot + be confirmed to exist. + + Parameters + ---------- + invoice_id : str + Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BillingInvoicePdfResponse] + Stripe-hosted PDF URL for the invoice. + """ + _response = self._client_wrapper.httpx_client.request( + f"payment/v1/invoices/{encode_path_param(invoice_id)}/pdf", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingInvoicePdfResponse, + construct_type( + type_=BillingInvoicePdfResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawBillingClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_balance( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BillingBalanceResponse]: + """ + Returns the organization's current credit balance in USD plus the + current plan identifier. Organization is resolved from the API key, + so no `X-Organization-Id` header is required. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BillingBalanceResponse] + Current balance for the caller's organization. + """ + _response = await self._client_wrapper.httpx_client.request( + "payment/v1/credits/balance", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingBalanceResponse, + construct_type( + type_=BillingBalanceResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_ledger( + self, + *, + limit: typing.Optional[int] = None, + offset: typing.Optional[int] = None, + from_: typing.Optional[dt.datetime] = None, + to: typing.Optional[dt.datetime] = None, + type: typing.Optional[GetLedgerBillingRequestType] = None, + scope: typing.Optional[GetLedgerBillingRequestScope] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[BillingLedgerResponse]: + """ + Paginated credit-ledger transaction history. Ledger reads are served + from ClickHouse; a rare storage-tier outage returns an empty + `transactions` array rather than a 5xx. + + **Window rules** + + - `from` defaults to `to - 7 days`, `to` defaults to now. + - The span between `from` and `to` cannot exceed **90 days**. A wider + span returns 400. Page through longer periods by making multiple + calls with shifted `from`/`to`. + - `from` cannot be earlier than **2026-03-02T00:00:00Z**. Older + historical data is not available via API; contact support for bulk + exports. + - `from > to` returns 400. + + **Filters** + + - `type` filters to a single transaction type. `PAYMENTS` is a + virtual filter that returns both `CREDIT_PURCHASE` and + `AUTO_RELOAD` rows. + - `scope` filters `USAGE_DEDUCTION` rows by product category. + + Parameters + ---------- + limit : typing.Optional[int] + Page size (1–100). + + offset : typing.Optional[int] + Offset for pagination. + + from_ : typing.Optional[dt.datetime] + Lower bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to seven days before `to`. Cannot be earlier than `2026-03-02T00:00:00Z`. + + to : typing.Optional[dt.datetime] + Upper bound of the query window (ISO 8601, `Z`-suffixed UTC recommended). Defaults to now. + + type : typing.Optional[GetLedgerBillingRequestType] + Filter to a single transaction type. `PAYMENTS` is a virtual filter that returns purchase-related rows (`CREDIT_PURCHASE` + `AUTO_RELOAD`). + + scope : typing.Optional[GetLedgerBillingRequestScope] + Filter by spend category. Applies only to `USAGE_DEDUCTION` rows. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BillingLedgerResponse] + Paginated ledger with summary + query window echo. + """ + _response = await self._client_wrapper.httpx_client.request( + "payment/v1/credits/ledger", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + params={ + "limit": limit, + "offset": offset, + "from": serialize_datetime(from_) if from_ is not None else None, + "to": serialize_datetime(to) if to is not None else None, + "type": type, + "scope": scope, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingLedgerResponse, + construct_type( + type_=BillingLedgerResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_usage_breakdown( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BillingUsageBreakdownResponse]: + """ + Total credits spent so far, split across the three product scopes. + The window is a cumulative snapshot from **2026-03-02T00:00:00Z** + (the platform's usage-tracking start date) up to the current + instant. Served from ClickHouse. No query parameters. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BillingUsageBreakdownResponse] + Spend split by scope. + """ + _response = await self._client_wrapper.httpx_client.request( + "payment/v1/credits/usage/breakdown", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingUsageBreakdownResponse, + construct_type( + type_=BillingUsageBreakdownResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def list_invoices( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BillingInvoiceListResponse]: + """ + Returns up to 20 of the most recent Stripe invoices for the caller's + organization. Each item is the raw Stripe `Invoice` object; use the + canonical Stripe reference at + [stripe.com/docs/api/invoices/object](https://stripe.com/docs/api/invoices/object) + for field-level semantics. + + Organizations that have never been charged (free-tier only) return + an empty array. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BillingInvoiceListResponse] + Up to 20 recent Stripe invoices for the caller's organization. + """ + _response = await self._client_wrapper.httpx_client.request( + "payment/v1/invoices", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingInvoiceListResponse, + construct_type( + type_=BillingInvoiceListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_invoice_pdf( + self, invoice_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BillingInvoicePdfResponse]: + """ + Returns a Stripe-hosted PDF URL for the invoice. The URL is + short-lived; fetch fresh when you need to hand it to a user. + + Requesting an invoice that does not belong to the caller's + organization returns 404 (not 403), so a foreign invoice ID cannot + be confirmed to exist. + + Parameters + ---------- + invoice_id : str + Stripe invoice ID (e.g. `in_1THiCSRwh8g1U6dfOcUtTdq9`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BillingInvoicePdfResponse] + Stripe-hosted PDF URL for the invoice. + """ + _response = await self._client_wrapper.httpx_client.request( + f"payment/v1/invoices/{encode_path_param(invoice_id)}/pdf", + base_url=self._client_wrapper.get_environment().payment, + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BillingInvoicePdfResponse, + construct_type( + type_=BillingInvoicePdfResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + construct_type( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/smallestai/atoms/billing/types/__init__.py b/src/smallestai/atoms/billing/types/__init__.py new file mode 100644 index 00000000..a692d8c9 --- /dev/null +++ b/src/smallestai/atoms/billing/types/__init__.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .get_ledger_billing_request_scope import GetLedgerBillingRequestScope + from .get_ledger_billing_request_type import GetLedgerBillingRequestType +_dynamic_imports: typing.Dict[str, str] = { + "GetLedgerBillingRequestScope": ".get_ledger_billing_request_scope", + "GetLedgerBillingRequestType": ".get_ledger_billing_request_type", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["GetLedgerBillingRequestScope", "GetLedgerBillingRequestType"] diff --git a/src/smallestai/atoms/billing/types/get_ledger_billing_request_scope.py b/src/smallestai/atoms/billing/types/get_ledger_billing_request_scope.py new file mode 100644 index 00000000..48a4e50c --- /dev/null +++ b/src/smallestai/atoms/billing/types/get_ledger_billing_request_scope.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +GetLedgerBillingRequestScope = typing.Union[typing.Literal["platform", "voice_ai", "voice_models"], typing.Any] diff --git a/src/smallestai/atoms/billing/types/get_ledger_billing_request_type.py b/src/smallestai/atoms/billing/types/get_ledger_billing_request_type.py new file mode 100644 index 00000000..51f02ca4 --- /dev/null +++ b/src/smallestai/atoms/billing/types/get_ledger_billing_request_type.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +GetLedgerBillingRequestType = typing.Union[ + typing.Literal[ + "SIGNUP_BONUS", + "CREDIT_PURCHASE", + "AUTO_RELOAD", + "USAGE_DEDUCTION", + "ADMIN_ADJUSTMENT", + "COUPON_CREDIT", + "MIGRATION", + "PAYMENTS", + ], + typing.Any, +] diff --git a/src/smallestai/atoms/calls/types/get_calls_response_data_transcript_item.py b/src/smallestai/atoms/calls/types/get_calls_response_data_transcript_item.py index c6543ec8..74cb927f 100644 --- a/src/smallestai/atoms/calls/types/get_calls_response_data_transcript_item.py +++ b/src/smallestai/atoms/calls/types/get_calls_response_data_transcript_item.py @@ -19,9 +19,9 @@ class GetCallsResponseDataTranscriptItem(UncheckedBaseModel): The spoken text for this turn """ - timestamp: typing.Optional[float] = pydantic.Field(default=None) + timestamp: typing.Optional[str] = pydantic.Field(default=None) """ - Timestamp offset in milliseconds from call start + ISO-8601 timestamp of the turn (e.g. 2026-07-28T15:46:12.472Z) """ if IS_PYDANTIC_V2: diff --git a/src/smallestai/atoms/client.py b/src/smallestai/atoms/client.py index ffe5c889..96f20119 100644 --- a/src/smallestai/atoms/client.py +++ b/src/smallestai/atoms/client.py @@ -16,6 +16,7 @@ from .agents.client import AgentsClient, AsyncAgentsClient from .analytics.client import AnalyticsClient, AsyncAnalyticsClient from .audience.client import AsyncAudienceClient, AudienceClient + from .billing.client import AsyncBillingClient, BillingClient from .call_actions.client import AsyncCallActionsClient, CallActionsClient from .calls.client import AsyncCallsClient, CallsClient from .campaigns.client import AsyncCampaignsClient, CampaignsClient @@ -67,6 +68,7 @@ def __init__(self, *, client_wrapper: SyncClientWrapper): self._concurrency: typing.Optional[ConcurrencyClient] = None self._disposition_metric_templates: typing.Optional[DispositionMetricTemplatesClient] = None self._dnc: typing.Optional[DncClient] = None + self._billing: typing.Optional[BillingClient] = None @property def with_raw_response(self) -> RawAtomsClient: @@ -279,6 +281,14 @@ def dnc(self): self._dnc = DncClient(client_wrapper=self._client_wrapper) return self._dnc + @property + def billing(self): + if self._billing is None: + from .billing.client import BillingClient # noqa: E402 + + self._billing = BillingClient(client_wrapper=self._client_wrapper) + return self._billing + class AsyncAtomsClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): @@ -309,6 +319,7 @@ def __init__(self, *, client_wrapper: AsyncClientWrapper): self._concurrency: typing.Optional[AsyncConcurrencyClient] = None self._disposition_metric_templates: typing.Optional[AsyncDispositionMetricTemplatesClient] = None self._dnc: typing.Optional[AsyncDncClient] = None + self._billing: typing.Optional[AsyncBillingClient] = None @property def with_raw_response(self) -> AsyncRawAtomsClient: @@ -522,3 +533,11 @@ def dnc(self): self._dnc = AsyncDncClient(client_wrapper=self._client_wrapper) return self._dnc + + @property + def billing(self): + if self._billing is None: + from .billing.client import AsyncBillingClient # noqa: E402 + + self._billing = AsyncBillingClient(client_wrapper=self._client_wrapper) + return self._billing diff --git a/src/smallestai/atoms/types/__init__.py b/src/smallestai/atoms/types/__init__.py index b7ed31ee..b66c2646 100644 --- a/src/smallestai/atoms/types/__init__.py +++ b/src/smallestai/atoms/types/__init__.py @@ -53,7 +53,21 @@ from .analytics_trend_metric import AnalyticsTrendMetric from .api_response import ApiResponse from .bad_request_error_body import BadRequestErrorBody + from .bad_request_error_body_error import BadRequestErrorBodyError from .bad_request_error_response import BadRequestErrorResponse + from .billing_balance_response import BillingBalanceResponse + from .billing_balance_response_data import BillingBalanceResponseData + from .billing_invoice_list_response import BillingInvoiceListResponse + from .billing_invoice_pdf_response import BillingInvoicePdfResponse + from .billing_invoice_pdf_response_data import BillingInvoicePdfResponseData + from .billing_ledger_entry import BillingLedgerEntry + from .billing_ledger_entry_transaction_type import BillingLedgerEntryTransactionType + from .billing_ledger_response import BillingLedgerResponse + from .billing_ledger_response_data import BillingLedgerResponseData + from .billing_ledger_response_data_period import BillingLedgerResponseDataPeriod + from .billing_ledger_response_data_summary import BillingLedgerResponseDataSummary + from .billing_usage_breakdown_response import BillingUsageBreakdownResponse + from .billing_usage_breakdown_response_data import BillingUsageBreakdownResponseData from .branch import Branch from .branch_status import BranchStatus from .branch_summary import BranchSummary @@ -92,6 +106,7 @@ from .locked_error_response import LockedErrorResponse from .locked_error_response_error_type import LockedErrorResponseErrorType from .not_found_error_body import NotFoundErrorBody + from .not_found_error_body_error import NotFoundErrorBodyError from .post_call_analytics_config import PostCallAnalyticsConfig from .post_call_analytics_config_success_metrics_item import PostCallAnalyticsConfigSuccessMetricsItem from .post_call_analytics_config_success_metrics_item_success_metric_type import ( @@ -226,7 +241,21 @@ "AnalyticsTrendMetric": ".analytics_trend_metric", "ApiResponse": ".api_response", "BadRequestErrorBody": ".bad_request_error_body", + "BadRequestErrorBodyError": ".bad_request_error_body_error", "BadRequestErrorResponse": ".bad_request_error_response", + "BillingBalanceResponse": ".billing_balance_response", + "BillingBalanceResponseData": ".billing_balance_response_data", + "BillingInvoiceListResponse": ".billing_invoice_list_response", + "BillingInvoicePdfResponse": ".billing_invoice_pdf_response", + "BillingInvoicePdfResponseData": ".billing_invoice_pdf_response_data", + "BillingLedgerEntry": ".billing_ledger_entry", + "BillingLedgerEntryTransactionType": ".billing_ledger_entry_transaction_type", + "BillingLedgerResponse": ".billing_ledger_response", + "BillingLedgerResponseData": ".billing_ledger_response_data", + "BillingLedgerResponseDataPeriod": ".billing_ledger_response_data_period", + "BillingLedgerResponseDataSummary": ".billing_ledger_response_data_summary", + "BillingUsageBreakdownResponse": ".billing_usage_breakdown_response", + "BillingUsageBreakdownResponseData": ".billing_usage_breakdown_response_data", "Branch": ".branch", "BranchStatus": ".branch_status", "BranchSummary": ".branch_summary", @@ -265,6 +294,7 @@ "LockedErrorResponse": ".locked_error_response", "LockedErrorResponseErrorType": ".locked_error_response_error_type", "NotFoundErrorBody": ".not_found_error_body", + "NotFoundErrorBodyError": ".not_found_error_body_error", "PostCallAnalyticsConfig": ".post_call_analytics_config", "PostCallAnalyticsConfigSuccessMetricsItem": ".post_call_analytics_config_success_metrics_item", "PostCallAnalyticsConfigSuccessMetricsItemSuccessMetricType": ".post_call_analytics_config_success_metrics_item_success_metric_type", @@ -421,7 +451,21 @@ def __dir__(): "AnalyticsTrendMetric", "ApiResponse", "BadRequestErrorBody", + "BadRequestErrorBodyError", "BadRequestErrorResponse", + "BillingBalanceResponse", + "BillingBalanceResponseData", + "BillingInvoiceListResponse", + "BillingInvoicePdfResponse", + "BillingInvoicePdfResponseData", + "BillingLedgerEntry", + "BillingLedgerEntryTransactionType", + "BillingLedgerResponse", + "BillingLedgerResponseData", + "BillingLedgerResponseDataPeriod", + "BillingLedgerResponseDataSummary", + "BillingUsageBreakdownResponse", + "BillingUsageBreakdownResponseData", "Branch", "BranchStatus", "BranchSummary", @@ -460,6 +504,7 @@ def __dir__(): "LockedErrorResponse", "LockedErrorResponseErrorType", "NotFoundErrorBody", + "NotFoundErrorBodyError", "PostCallAnalyticsConfig", "PostCallAnalyticsConfigSuccessMetricsItem", "PostCallAnalyticsConfigSuccessMetricsItemSuccessMetricType", diff --git a/src/smallestai/atoms/types/bad_request_error_body.py b/src/smallestai/atoms/types/bad_request_error_body.py index ad9708f7..e1913d82 100644 --- a/src/smallestai/atoms/types/bad_request_error_body.py +++ b/src/smallestai/atoms/types/bad_request_error_body.py @@ -5,11 +5,12 @@ import pydantic from ...core.pydantic_utilities import IS_PYDANTIC_V2 from ...core.unchecked_base_model import UncheckedBaseModel +from .bad_request_error_body_error import BadRequestErrorBodyError class BadRequestErrorBody(UncheckedBaseModel): - status: typing.Optional[bool] = None - errors: typing.Optional[typing.List[str]] = None + success: typing.Optional[bool] = None + error: typing.Optional[BadRequestErrorBodyError] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/smallestai/atoms/types/bad_request_error_body_error.py b/src/smallestai/atoms/types/bad_request_error_body_error.py new file mode 100644 index 00000000..b4451b95 --- /dev/null +++ b/src/smallestai/atoms/types/bad_request_error_body_error.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.unchecked_base_model import UncheckedBaseModel + + +class BadRequestErrorBodyError(UncheckedBaseModel): + code: typing.Optional[str] = None + message: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_balance_response.py b/src/smallestai/atoms/types/billing_balance_response.py new file mode 100644 index 00000000..2c4c1dec --- /dev/null +++ b/src/smallestai/atoms/types/billing_balance_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.unchecked_base_model import UncheckedBaseModel +from .billing_balance_response_data import BillingBalanceResponseData + + +class BillingBalanceResponse(UncheckedBaseModel): + success: bool + data: BillingBalanceResponseData + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_balance_response_data.py b/src/smallestai/atoms/types/billing_balance_response_data.py new file mode 100644 index 00000000..cf98217e --- /dev/null +++ b/src/smallestai/atoms/types/billing_balance_response_data.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.serialization import FieldMetadata +from ...core.unchecked_base_model import UncheckedBaseModel + + +class BillingBalanceResponseData(UncheckedBaseModel): + credit_balance: typing_extensions.Annotated[ + float, + FieldMetadata(alias="creditBalance"), + pydantic.Field( + alias="creditBalance", + description="Current credit balance in USD. May be negative if the account has spent past its balance and is on a plan that permits overdraw.", + ), + ] + plan_id: typing_extensions.Annotated[ + str, + FieldMetadata(alias="planId"), + pydantic.Field(alias="planId", description="Identifier for the plan the organization is on."), + ] + is_enterprise: typing_extensions.Annotated[ + bool, + FieldMetadata(alias="isEnterprise"), + pydantic.Field( + alias="isEnterprise", description="True when the organization is on an enterprise plan (billed offline)." + ), + ] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_invoice_list_response.py b/src/smallestai/atoms/types/billing_invoice_list_response.py new file mode 100644 index 00000000..04dffa3a --- /dev/null +++ b/src/smallestai/atoms/types/billing_invoice_list_response.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.unchecked_base_model import UncheckedBaseModel + + +class BillingInvoiceListResponse(UncheckedBaseModel): + success: bool + data: typing.List[typing.Dict[str, typing.Any]] = pydantic.Field() + """ + Up to 20 recent Stripe `Invoice` objects for the caller's organization. Fields follow the canonical Stripe shape. Keys most callers use: `id`, `status`, `amount_paid`, `amount_due`, `currency`, `created`, `hosted_invoice_url`, `invoice_pdf`, `number`. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_invoice_pdf_response.py b/src/smallestai/atoms/types/billing_invoice_pdf_response.py new file mode 100644 index 00000000..eaec093c --- /dev/null +++ b/src/smallestai/atoms/types/billing_invoice_pdf_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.unchecked_base_model import UncheckedBaseModel +from .billing_invoice_pdf_response_data import BillingInvoicePdfResponseData + + +class BillingInvoicePdfResponse(UncheckedBaseModel): + success: bool + data: BillingInvoicePdfResponseData + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_invoice_pdf_response_data.py b/src/smallestai/atoms/types/billing_invoice_pdf_response_data.py new file mode 100644 index 00000000..b9a60436 --- /dev/null +++ b/src/smallestai/atoms/types/billing_invoice_pdf_response_data.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.serialization import FieldMetadata +from ...core.unchecked_base_model import UncheckedBaseModel + + +class BillingInvoicePdfResponseData(UncheckedBaseModel): + pdf_url: typing_extensions.Annotated[ + str, + FieldMetadata(alias="pdfUrl"), + pydantic.Field( + alias="pdfUrl", + description="Stripe-hosted URL to the invoice PDF. Short-lived; refetch when you need to hand it to a user.", + ), + ] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_ledger_entry.py b/src/smallestai/atoms/types/billing_ledger_entry.py new file mode 100644 index 00000000..cc5d93c0 --- /dev/null +++ b/src/smallestai/atoms/types/billing_ledger_entry.py @@ -0,0 +1,73 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.serialization import FieldMetadata +from ...core.unchecked_base_model import UncheckedBaseModel +from .billing_ledger_entry_transaction_type import BillingLedgerEntryTransactionType + + +class BillingLedgerEntry(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Ledger row identifier. + """ + + transaction_type: typing_extensions.Annotated[ + BillingLedgerEntryTransactionType, + FieldMetadata(alias="transactionType"), + pydantic.Field(alias="transactionType"), + ] + amount: float = pydantic.Field() + """ + Signed credit delta in USD. Positive for additions (`CREDIT_PURCHASE`, `AUTO_RELOAD`, `COUPON_CREDIT`, `SIGNUP_BONUS`), negative for spend (`USAGE_DEDUCTION`). + """ + + balance_after: typing_extensions.Annotated[ + float, + FieldMetadata(alias="balanceAfter"), + pydantic.Field(alias="balanceAfter", description="Balance after this row was applied."), + ] + feature_id: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="featureId"), + pydantic.Field( + alias="featureId", + description="Product feature that triggered the row (e.g. `tts.generate`, `waves.stt`). Only set on `USAGE_DEDUCTION`.", + ), + ] = None + reference_id: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="referenceId"), + pydantic.Field( + alias="referenceId", description="External reference tying this row to a purchase, coupon, or admin action." + ), + ] = None + description: typing.Optional[str] = None + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt"), pydantic.Field(alias="createdAt") + ] + call_id: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="callId"), + pydantic.Field(alias="callId", description="Associated call ID, when the row was produced by a call."), + ] = None + agent_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="agentId"), pydantic.Field(alias="agentId") + ] = None + request_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="requestId"), pydantic.Field(alias="requestId") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_ledger_entry_transaction_type.py b/src/smallestai/atoms/types/billing_ledger_entry_transaction_type.py new file mode 100644 index 00000000..7fb73a75 --- /dev/null +++ b/src/smallestai/atoms/types/billing_ledger_entry_transaction_type.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +BillingLedgerEntryTransactionType = typing.Union[ + typing.Literal[ + "SIGNUP_BONUS", + "CREDIT_PURCHASE", + "AUTO_RELOAD", + "USAGE_DEDUCTION", + "ADMIN_ADJUSTMENT", + "COUPON_CREDIT", + "MIGRATION", + ], + typing.Any, +] diff --git a/src/smallestai/atoms/types/billing_ledger_response.py b/src/smallestai/atoms/types/billing_ledger_response.py new file mode 100644 index 00000000..1a1b166a --- /dev/null +++ b/src/smallestai/atoms/types/billing_ledger_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.unchecked_base_model import UncheckedBaseModel +from .billing_ledger_response_data import BillingLedgerResponseData + + +class BillingLedgerResponse(UncheckedBaseModel): + success: bool + data: BillingLedgerResponseData + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_ledger_response_data.py b/src/smallestai/atoms/types/billing_ledger_response_data.py new file mode 100644 index 00000000..ed30430a --- /dev/null +++ b/src/smallestai/atoms/types/billing_ledger_response_data.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.serialization import FieldMetadata +from ...core.unchecked_base_model import UncheckedBaseModel +from .billing_ledger_entry import BillingLedgerEntry +from .billing_ledger_response_data_period import BillingLedgerResponseDataPeriod +from .billing_ledger_response_data_summary import BillingLedgerResponseDataSummary + + +class BillingLedgerResponseData(UncheckedBaseModel): + transactions: typing.List[BillingLedgerEntry] + total: int = pydantic.Field() + """ + Total row count matching the filters (across all pages). + """ + + has_more: typing_extensions.Annotated[ + bool, + FieldMetadata(alias="hasMore"), + pydantic.Field(alias="hasMore", description="True when more rows exist beyond `offset + limit`."), + ] + summary: BillingLedgerResponseDataSummary + period: BillingLedgerResponseDataPeriod + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_ledger_response_data_period.py b/src/smallestai/atoms/types/billing_ledger_response_data_period.py new file mode 100644 index 00000000..bcb46d4f --- /dev/null +++ b/src/smallestai/atoms/types/billing_ledger_response_data_period.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.serialization import FieldMetadata +from ...core.unchecked_base_model import UncheckedBaseModel + + +class BillingLedgerResponseDataPeriod(UncheckedBaseModel): + from_: typing_extensions.Annotated[ + dt.datetime, + FieldMetadata(alias="from"), + pydantic.Field( + alias="from", + description="Echo of the effective lower bound (defaults to seven days before `to` when not supplied).", + ), + ] + to: dt.datetime = pydantic.Field() + """ + Echo of the effective upper bound (defaults to server-now when not supplied). + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_ledger_response_data_summary.py b/src/smallestai/atoms/types/billing_ledger_response_data_summary.py new file mode 100644 index 00000000..7640eb5a --- /dev/null +++ b/src/smallestai/atoms/types/billing_ledger_response_data_summary.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.serialization import FieldMetadata +from ...core.unchecked_base_model import UncheckedBaseModel + + +class BillingLedgerResponseDataSummary(UncheckedBaseModel): + total_credits_consumed: typing_extensions.Annotated[ + float, + FieldMetadata(alias="totalCreditsConsumed"), + pydantic.Field( + alias="totalCreditsConsumed", description="Sum of `USAGE_DEDUCTION` magnitudes across the window." + ), + ] + total_credits_added: typing_extensions.Annotated[ + float, + FieldMetadata(alias="totalCreditsAdded"), + pydantic.Field( + alias="totalCreditsAdded", + description="Sum of positive rows (purchases, auto-reloads, coupon credits, signup bonuses) across the window.", + ), + ] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_usage_breakdown_response.py b/src/smallestai/atoms/types/billing_usage_breakdown_response.py new file mode 100644 index 00000000..86480427 --- /dev/null +++ b/src/smallestai/atoms/types/billing_usage_breakdown_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.unchecked_base_model import UncheckedBaseModel +from .billing_usage_breakdown_response_data import BillingUsageBreakdownResponseData + + +class BillingUsageBreakdownResponse(UncheckedBaseModel): + success: bool + data: BillingUsageBreakdownResponseData + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/billing_usage_breakdown_response_data.py b/src/smallestai/atoms/types/billing_usage_breakdown_response_data.py new file mode 100644 index 00000000..0090c911 --- /dev/null +++ b/src/smallestai/atoms/types/billing_usage_breakdown_response_data.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.serialization import FieldMetadata +from ...core.unchecked_base_model import UncheckedBaseModel + + +class BillingUsageBreakdownResponseData(UncheckedBaseModel): + platform: float = pydantic.Field() + """ + Credits spent on Atoms platform features (phone rental, telephony minutes, etc.). + """ + + voice_ai: typing_extensions.Annotated[ + float, + FieldMetadata(alias="voiceAi"), + pydantic.Field( + alias="voiceAi", description="Credits spent on voice-agent runtime (LLM + TTS combined during voice calls)." + ), + ] + voice_models: typing_extensions.Annotated[ + float, + FieldMetadata(alias="voiceModels"), + pydantic.Field( + alias="voiceModels", + description="Credits spent on standalone Waves model calls (Lightning TTS, Pulse STT, Electron LLM used outside the voice-agent pipeline).", + ), + ] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/atoms/types/not_found_error_body.py b/src/smallestai/atoms/types/not_found_error_body.py index 374b0fc6..09b9a676 100644 --- a/src/smallestai/atoms/types/not_found_error_body.py +++ b/src/smallestai/atoms/types/not_found_error_body.py @@ -5,11 +5,12 @@ import pydantic from ...core.pydantic_utilities import IS_PYDANTIC_V2 from ...core.unchecked_base_model import UncheckedBaseModel +from .not_found_error_body_error import NotFoundErrorBodyError class NotFoundErrorBody(UncheckedBaseModel): - status: typing.Optional[bool] = None - errors: typing.Optional[typing.List[str]] = None + success: typing.Optional[bool] = None + error: typing.Optional[NotFoundErrorBodyError] = None if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/smallestai/atoms/types/not_found_error_body_error.py b/src/smallestai/atoms/types/not_found_error_body_error.py new file mode 100644 index 00000000..0dcf1c4e --- /dev/null +++ b/src/smallestai/atoms/types/not_found_error_body_error.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from ...core.unchecked_base_model import UncheckedBaseModel + + +class NotFoundErrorBodyError(UncheckedBaseModel): + code: typing.Optional[str] = None + message: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/smallestai/environment.py b/src/smallestai/environment.py index c5f76703..a3fa4a47 100644 --- a/src/smallestai/environment.py +++ b/src/smallestai/environment.py @@ -6,12 +6,16 @@ class SmallestAIEnvironment: PRODUCTION: SmallestAIEnvironment - def __init__(self, *, atoms: str, waves: str, waves_ws: str): + def __init__(self, *, atoms: str, waves: str, waves_ws: str, payment: str): self.atoms = atoms self.waves = waves self.waves_ws = waves_ws + self.payment = payment SmallestAIEnvironment.PRODUCTION = SmallestAIEnvironment( - atoms="https://api.smallest.ai/atoms/v1", waves="https://api.smallest.ai", waves_ws="wss://api.smallest.ai" + atoms="https://api.smallest.ai/atoms/v1", + waves="https://api.smallest.ai", + waves_ws="wss://api.smallest.ai", + payment="https://api.smallest.ai", ) diff --git a/tests/wire/conftest.py b/tests/wire/conftest.py index 5646a37b..9fd029ef 100644 --- a/tests/wire/conftest.py +++ b/tests/wire/conftest.py @@ -45,13 +45,13 @@ def get_client(test_id: str) -> SmallestAI: if _CLIENT_SUPPORTS_HEADERS: return SmallestAI( - environment=SmallestAIEnvironment(atoms=base_url, waves=base_url, waves_ws=base_url), + environment=SmallestAIEnvironment(atoms=base_url, waves=base_url, waves_ws=base_url, payment=base_url), headers=test_headers, api_key="test_token", ) return SmallestAI( - environment=SmallestAIEnvironment(atoms=base_url, waves=base_url, waves_ws=base_url), + environment=SmallestAIEnvironment(atoms=base_url, waves=base_url, waves_ws=base_url, payment=base_url), httpx_client=httpx.Client(headers=test_headers), api_key="test_token", ) diff --git a/tests/wire/test_atoms_billing.py b/tests/wire/test_atoms_billing.py new file mode 100644 index 00000000..11fe0e3d --- /dev/null +++ b/tests/wire/test_atoms_billing.py @@ -0,0 +1,50 @@ +import datetime + +from .conftest import get_client, verify_request_count + + +def test_atoms_billing_get_balance() -> None: + """Test get_balance endpoint with WireMock""" + test_id = "atoms.billing.get_balance.0" + client = get_client(test_id) + client.atoms.billing.get_balance() + verify_request_count(test_id, "GET", "/payment/v1/credits/balance", None, 1) + + +def test_atoms_billing_get_ledger() -> None: + """Test get_ledger endpoint with WireMock""" + test_id = "atoms.billing.get_ledger.0" + client = get_client(test_id) + client.atoms.billing.get_ledger( + from_=datetime.datetime.fromisoformat("2026-07-01T00:00:00+00:00"), + to=datetime.datetime.fromisoformat("2026-07-28T00:00:00+00:00"), + ) + verify_request_count( + test_id, "GET", "/payment/v1/credits/ledger", {"from": "2026-07-01T00:00:00Z", "to": "2026-07-28T00:00:00Z"}, 1 + ) + + +def test_atoms_billing_get_usage_breakdown() -> None: + """Test get_usage_breakdown endpoint with WireMock""" + test_id = "atoms.billing.get_usage_breakdown.0" + client = get_client(test_id) + client.atoms.billing.get_usage_breakdown() + verify_request_count(test_id, "GET", "/payment/v1/credits/usage/breakdown", None, 1) + + +def test_atoms_billing_list_invoices() -> None: + """Test list_invoices endpoint with WireMock""" + test_id = "atoms.billing.list_invoices.0" + client = get_client(test_id) + client.atoms.billing.list_invoices() + verify_request_count(test_id, "GET", "/payment/v1/invoices", None, 1) + + +def test_atoms_billing_get_invoice_pdf() -> None: + """Test get_invoice_pdf endpoint with WireMock""" + test_id = "atoms.billing.get_invoice_pdf.0" + client = get_client(test_id) + client.atoms.billing.get_invoice_pdf( + invoice_id="invoiceId", + ) + verify_request_count(test_id, "GET", "/payment/v1/invoices/invoiceId/pdf", None, 1) diff --git a/wiremock/wiremock-mappings.json b/wiremock/wiremock-mappings.json index abd0d9e2..97836e7e 100644 --- a/wiremock/wiremock-mappings.json +++ b/wiremock/wiremock-mappings.json @@ -4798,6 +4798,178 @@ }, "postServeActions": [] }, + { + "id": "7a585eda-d514-4d17-b714-44d3f13490ae", + "name": "Get credit balance - default", + "request": { + "urlPathTemplate": "/payment/v1/credits/balance", + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"success\": true,\n \"data\": {\n \"creditBalance\": 15,\n \"planId\": \"plan_standard\",\n \"isEnterprise\": false\n }\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "7a585eda-d514-4d17-b714-44d3f13490ae", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + }, + "postServeActions": [] + }, + { + "id": "f17826c8-8354-4351-9daf-cf57d2fa4647", + "name": "List credit ledger - default", + "request": { + "urlPathTemplate": "/payment/v1/credits/ledger", + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, + "queryParameters": { + "from": { + "equalTo": "2026-07-01T00:00:00Z" + }, + "to": { + "equalTo": "2026-07-28T00:00:00Z" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"success\": true,\n \"data\": {\n \"transactions\": [\n {\n \"id\": \"id\",\n \"transactionType\": \"SIGNUP_BONUS\",\n \"amount\": 1.1,\n \"balanceAfter\": 1.1,\n \"createdAt\": \"2024-01-15T09:30:00Z\"\n }\n ],\n \"total\": 1,\n \"hasMore\": true,\n \"summary\": {\n \"totalCreditsConsumed\": 1.1,\n \"totalCreditsAdded\": 1.1\n },\n \"period\": {\n \"from\": \"2024-01-15T09:30:00Z\",\n \"to\": \"2024-01-15T09:30:00Z\"\n }\n }\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "f17826c8-8354-4351-9daf-cf57d2fa4647", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + }, + "postServeActions": [] + }, + { + "id": "9c57aa7a-6bbc-4696-9d72-74558360bc33", + "name": "Get usage breakdown - default", + "request": { + "urlPathTemplate": "/payment/v1/credits/usage/breakdown", + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"success\": true,\n \"data\": {\n \"platform\": 1.1,\n \"voiceAi\": 1.1,\n \"voiceModels\": 1.1\n }\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "9c57aa7a-6bbc-4696-9d72-74558360bc33", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + }, + "postServeActions": [] + }, + { + "id": "658e481c-9633-4b82-877b-c6bb310a4218", + "name": "List invoices - default", + "request": { + "urlPathTemplate": "/payment/v1/invoices", + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"success\": true,\n \"data\": [\n {\n \"key\": \"value\"\n }\n ]\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "658e481c-9633-4b82-877b-c6bb310a4218", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + }, + "postServeActions": [] + }, + { + "id": "b67cb773-6ac8-4d0f-a178-05d1ed4984d6", + "name": "Get invoice PDF URL - default", + "request": { + "urlPathTemplate": "/payment/v1/invoices/{invoiceId}/pdf", + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, + "pathParameters": { + "invoiceId": { + "equalTo": "invoiceId" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"success\": true,\n \"data\": {\n \"pdfUrl\": \"pdfUrl\"\n }\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "b67cb773-6ac8-4d0f-a178-05d1ed4984d6", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, { "id": "23284c9e-d15e-4f3a-84b7-de9613ccf399", "name": "Get pronunciation dictionaries - default", @@ -5272,6 +5444,6 @@ } ], "meta": { - "total": 150 + "total": 155 } } \ No newline at end of file From a86206ff7b90089f47b1222c8e14e007564526c8 Mon Sep 17 00:00:00 2001 From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:32:03 +0000 Subject: [PATCH 2/3] [fern-replay] Applied customizations Patches applied (1): - patch-0c9e9b4e: refactor: rename agent-swarm -> agent-crew across SDK + CLI (4.4.2) --- .fern/replay.lock | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.fern/replay.lock b/.fern/replay.lock index ce1e26d2..0056a982 100644 --- a/.fern/replay.lock +++ b/.fern/replay.lock @@ -24,14 +24,20 @@ generations: cli_version: unknown generator_versions: fernapi/fern-python-sdk: 5.12.12 -current_generation: 58539514affc27c6340e570ffd7f18d6c6806433 + - commit_sha: 0a211d42874771b6c014819e8da42459bfdca96a + tree_hash: 8cb30599679840940039628b77c844caa2819777 + timestamp: 2026-07-28T18:32:00.540Z + cli_version: unknown + generator_versions: + fernapi/fern-python-sdk: 5.12.12 +current_generation: 0a211d42874771b6c014819e8da42459bfdca96a patches: - id: patch-0c9e9b4e content_hash: sha256:43cbb1c853a3de0a20a4d1e151169f4039eec0f4d998b32978cb5038f2a3b665 original_commit: 0c9e9b4e2286bd64653a1bdeb91a2b871a56668e original_message: "refactor: rename agent-swarm -> agent-crew across SDK + CLI (4.4.2)" original_author: Maharshi Chattopadhyay - base_generation: 58539514affc27c6340e570ffd7f18d6c6806433 + base_generation: 0a211d42874771b6c014819e8da42459bfdca96a files: - src/smallestai/atoms/swarm/__init__.py - src/smallestai/atoms/swarm/clients/__init__.py From 1891e3e6df3934e9e0d390aba5a293b7713754c0 Mon Sep 17 00:00:00 2001 From: Abhishek Mishra Date: Wed, 29 Jul 2026 00:05:56 +0530 Subject: [PATCH 3/3] =?UTF-8?q?release:=205.3.1=20=E2=80=94=20billing=20en?= =?UTF-8?q?dpoints,=20calls=20timestamp=20fix,=20agent-crew=20deploy=20Dev?= =?UTF-8?q?X?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump 5.3.0 -> 5.3.1 + changelog. - Billing rides the regen: client.atoms.billing (get_balance, get_ledger, get_usage_breakdown, list_invoices, get_invoice_pdf) on the payment base. - calls transcript timestamp now typed as string (ISO-8601), was number. - agent-crew deploy DevX (cli/, .fernignore'd): pre-deploy layout check (src/ layout, nested entry, pyproject-without-requirements) and a corrected env-var warning pointing to the .env + load_dotenv path. - Pass payment= to the four hand-written SmallestAIEnvironment constructions (examples + velocity tests) now that the env has a payment url. Gauntlet on the built wheel: mypy clean, scripts/verify.py PASSED, custom suite 41 passed, fresh-install imports + surface OK (atoms.billing present). --- changelog.md | 14 ++++++++++ examples/build_voice_agent.py | 1 + examples/quickstart.py | 1 + pyproject.toml | 2 +- src/smallestai/cli/agent_crew.py | 41 +++++++++++++++++++++++++---- tests/velocity/triage.py | 2 +- tests/velocity/v2_versioning_e2e.py | 2 +- 7 files changed, 55 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index 26a301d2..60f63abc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,17 @@ +## 5.3.1 - 2026-07-29 + +Billing read endpoints, a call-log type fix, and clearer agent-crew deploy warnings. Additive; no breaking changes. + +* **Billing** (new `client.atoms.billing`): `get_balance`, `get_ledger`, `get_usage_breakdown`, + `list_invoices`, `get_invoice_pdf` (read-only credit and invoice endpoints under `/payment/v1`). +* **Fix**: `calls.get` / `calls.list` transcript-turn `timestamp` is typed as a string (ISO-8601), + matching what the API returns. Previously typed as a number, which triggered a deserialization + warning. +* **CLI DevX** (`smallestai agent-crew deploy`): a pre-deploy layout check warns on a `src/` layout, + a nested entry point, or a `pyproject.toml` with no `requirements.txt` (all fail the cloud build); + and the environment-variable warning now points to the working path (ship a `.env` at the project + root and call `load_dotenv()`; it deploys with your code) instead of just saying it is not propagated. + ## 5.3.0 - 2026-07-28 Unified speech-to-text namespace (breaking), voice cloning, Electron LLM, webhook update. diff --git a/examples/build_voice_agent.py b/examples/build_voice_agent.py index fb9fbd4c..3e0b18ba 100644 --- a/examples/build_voice_agent.py +++ b/examples/build_voice_agent.py @@ -39,6 +39,7 @@ def make_client() -> SmallestAI: atoms=f"{base}/atoms/v1", waves=base, waves_ws=base.replace("https", "wss").replace("http", "ws"), + payment=base, ) return SmallestAI(api_key=api_key, environment=env) return SmallestAI(api_key=api_key) diff --git a/examples/quickstart.py b/examples/quickstart.py index 5e1038bd..8dfceb04 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -25,6 +25,7 @@ def make_client() -> SmallestAI: atoms=f"{base}/atoms/v1", waves=base, waves_ws=base.replace("https", "wss").replace("http", "ws"), + payment=base, ) return SmallestAI(api_key=api_key, environment=env) return SmallestAI(api_key=api_key) # production diff --git a/pyproject.toml b/pyproject.toml index 39a734b7..489e220e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ dynamic = ["version"] [tool.poetry] name = "smallestai" -version = "5.3.0" +version = "5.3.1" description = "" readme = "README.md" authors = [] diff --git a/src/smallestai/cli/agent_crew.py b/src/smallestai/cli/agent_crew.py index 84408ef9..4e44599c 100644 --- a/src/smallestai/cli/agent_crew.py +++ b/src/smallestai/cli/agent_crew.py @@ -175,6 +175,36 @@ async def async_deploy(directory: str, entry_point: str): console.print(f"[dim]Entry point: {entry_point}[/dim]") console.print(f"[dim]Agent ID: {agent_id}[/dim]\n") + # Pre-flight layout check. The cloud build runs `pip install .` from a + # pyproject before copying your source, so a src/ layout (or a nested + # entry point) fails there. The supported path is a flat directory with a + # requirements.txt (what the cookbook examples ship). Warn early instead + # of letting the build fail opaquely. + has_pyproject = (dir_path / "pyproject.toml").exists() + has_requirements = (dir_path / "requirements.txt").exists() + has_src_dir = (dir_path / "src").is_dir() + nested_entry = ("/" in entry_point) or ("\\" in entry_point) + if (has_pyproject and not has_requirements) or has_src_dir or nested_entry: + console.print("[yellow]⚠ Project layout may not build in the cloud:[/yellow]") + if has_src_dir or nested_entry: + console.print( + " [yellow]• src/ layout / nested entry point detected. The cloud " + "build expects a flat directory with the entry file at the root " + "(e.g. server.py), not under src/.[/yellow]" + ) + if has_pyproject and not has_requirements: + console.print( + " [yellow]• pyproject.toml with no requirements.txt. The builder " + "runs `pip install .` before your source is copied, which fails for " + "src/ layouts. Add a requirements.txt (the builder prefers it and " + "skips that step).[/yellow]" + ) + console.print( + "[dim]Supported layout: a flat directory with server.py + a " + "requirements.txt at the root. See the cookbook getting_started " + "example.[/dim]\n" + ) + # Scan user code for env var references and warn — the deploy pipeline # does not yet propagate `.env` values into the pod, so any env var # the code reads must either be baked into the image or the call will @@ -190,11 +220,12 @@ async def async_deploy(directory: str, entry_point: str): for var in sorted(required_env_vars): console.print(f" [yellow]• {var}[/yellow]") console.print( - "[dim]The deploy pipeline does not propagate `.env` values into the " - "running pod yet. If these aren't set in the runtime image, your code " - "will fail at first use (e.g. `openai.OpenAIError: Missing credentials`). " - "Track: env-var propagation is the only outstanding blocker for many " - "examples in cookbook.[/dim]\n" + "[dim]Platform-level env injection isn't available yet, but there is a " + "working path: put these in a `.env` file at your project root and call " + "`load_dotenv()` at startup (the cookbook examples already do). The `.env` " + "ships with your deploy and is loaded in the pod, so your code reads them " + "via `os.getenv(...)`. Without it, calls fail at first use " + "(e.g. `openai.OpenAIError: Missing credentials`).[/dim]\n" ) # Create zip file in memory diff --git a/tests/velocity/triage.py b/tests/velocity/triage.py index 534b4aef..30f1d040 100644 --- a/tests/velocity/triage.py +++ b/tests/velocity/triage.py @@ -29,7 +29,7 @@ def agent_id_helper(): from smallestai import SmallestAI from smallestai.environment import SmallestAIEnvironment base = os.environ["SMALLEST_BASE_URL"].rstrip("/") - env = SmallestAIEnvironment(atoms=f"{base}/atoms/v1", waves=base, waves_ws=base.replace("https", "wss")) + env = SmallestAIEnvironment(atoms=f"{base}/atoms/v1", waves=base, waves_ws=base.replace("https", "wss"), payment=base) saved = {k: os.environ.pop(k, None) for k in ("SMALLEST_AI_TOKEN",)} os.environ["SMALLEST_API_KEY"] = os.environ["SMALLEST_API_KEY"] c2 = SmallestAI(environment=env) # no api_key arg -> must read SMALLEST_API_KEY diff --git a/tests/velocity/v2_versioning_e2e.py b/tests/velocity/v2_versioning_e2e.py index e7e5f5e0..24e4bb89 100644 --- a/tests/velocity/v2_versioning_e2e.py +++ b/tests/velocity/v2_versioning_e2e.py @@ -40,7 +40,7 @@ def make_client() -> SmallestAI: if BASE: host = BASE.split("/atoms")[0] ws = host.replace("https://", "wss://").replace("http://", "ws://") - env = SmallestAIEnvironment(atoms=BASE, waves=host, waves_ws=ws) + env = SmallestAIEnvironment(atoms=BASE, waves=host, waves_ws=ws, payment=host) return SmallestAI(environment=env, api_key=KEY) return SmallestAI(api_key=KEY)