Skip to main content
When T3Planet Credits mode is active, AiServiceInterface routes complete(), stream(), and embed() through the T3Planet composer API (Charge / Stream / Embed). Billing is token-based on the server — there are no fixed credits per feature_key in Features.php.

Credit mode changes (streaming)

The following applies when credits mode is on (see T3Planet Credits for activation):
  • POST /API/AI/Charge.phpAiServiceInterface::complete() returns AiResponse plus CreditsUsage.
  • POST /API/AI/Stream.phpAiServiceInterface::stream() returns incremental string chunks; read StreamSummary via getReturn().
  • POST /API/AI/Embed.phpAiServiceInterface::embed() returns EmbeddingResponse plus CreditsUsage.
  • POST /API/AI/Abort.php — called automatically when a stream is cancelled or ends without settlement (see Abort on cancel).
Streaming uses the same JSON body as Charge (domain, request_uuid, feature_key, meta_json, optional top-level extension_key). The server emits normalized SSE (not raw OpenAI): token deltas, optional ping, mandatory usage settlement, optional done. Debit runs after the stream ends from real token usage (post-settle), like Charge. When credits mode is off, stream() uses local adapters unchanged (generator return is void).

Token billing model

  • The server debits after upstream AI using actual token usage: credits = max(1, ceil(total_tokens / tokens_per_credit)).
  • Charge / Stream: total_tokens = tokens_input + tokens_output.
  • Embed: total_tokens = tokens_input.
  • Default 1 credit = 1000 tokens (overridable server-side via tokens_per_credit in the API pricing object).
Do not hardcode per-feature credit costs in child extensions. Use Pre-submit estimate (child extensions) before submit and read CreditsUsage after the call for the actual debit.

Pre-submit estimate (child extensions)

Before calling complete(), stream(), or embed(), call Estimate.php with the same feature_key and meta_json you will send to Charge/Stream/Embed. Use endpoint charge for completion and streaming; embed for embeddings. The response estimated_credits is approximate (shown as “≈ X credits”), not guaranteed. Inject the public service:
Estimate request shape (handled by CreditsEstimateService):
  • feature_key — extension-local key in AiOptions; normalized to composer catalog keys before the API call.
  • meta_json — must include prompt (and any fields your feature sends).
  • endpointcharge (default) or embed.

Feature key mapping (all extensions)

Child extensions keep their own telemetry keys in AiOptions::featureKey (for example seo.meta_description or translation.openai). In credits mode, CreditsFeatureKeyMapper maps them to composer ns_ai_feature_cost.feature_key values (seo_meta_description, content_translation, …) immediately before Charge / Stream / Embed. The original caller key is copied to meta_json.client_feature_key for cross-extension analytics. Register additional aliases in your extension ext_localconf.php:
Catalog constants live in CreditsFeatureKeyCatalog. Stream requests always bill as stream; embed requests as embedding.

Streaming (Stream.php)

Inject AiServiceInterface only — routing is handled by T3PlanetCreditAiService and ProxyAiExecutor.

SSE events (server → client)

  • token — Append data.delta to the UI buffer (incremental, not cumulative).
  • ping — Ignore (keepalive).
  • usage — Mandatory settlement; builds StreamSummary and CreditsUsage.
  • done — Optional; the stream may end after usage only.
If any token was received but the connection ends without usage, the client throws CreditsApiException — never treat that as success. Pre-stream errors (JSON, not SSE) use the same codes as Charge (402 insufficient credits, 409 stream_in_progress, 422 feature_unknown, etc.).

Abort on cancel

When the user cancels, closes the tab, or the PHP request aborts (connection_aborted()), ProxyAiExecutor stops reading SSE and calls POST /API/AI/Abort.php with the same request_uuid (within a few seconds). The server returns proportional cost_units / credits for partial output; the client may store a receipt row for the dashboard. Call Abort explicitly only if you implement custom cancel UX outside AiServiceInterface.

Idempotency

  • Reuse the same AiOptions::$requestUuid on network retry after failure.
  • After a completed stream, the server may replay one token (full content) + usage — no second charge.
  • 409 stream_in_progress: another stream with the same UUID is still open on the server. The client throws CreditsApiException (no auto-retry). Wait for the first stream to finish and retry the same UUID, or use a new UUID for a parallel request.
Do not send embed-only inputs[] in meta_json for Stream.

Caller attribution (Charge / Stream / Embed / Estimate)

Always set extensionKey on AiOptions (e.g. ns_news_comments). In credits mode, CreditsMetaJsonBuilder adds these fields to meta_json (and duplicates extension_key at the top level of the API body for forward-compatible servers):
  • extension_key — TYPO3 extension that initiated the call (not the license product).
  • feature_label, request_source, content_entity_type, content_entity_uid when set.
For manual estimates, pass the same AiOptions as the 4th argument to CreditsEstimateService::estimate() or call CreditsMetaJsonBuilder::withAttribution($meta, $options). Until the license server persists caller_extension_key, the usage UI may still show extension_key from ns_product_license (the product tied to license_key_used).

Backend module JavaScript (optional)

Requires TYPO3 backend AJAX route nst3af_credits_estimate and an active credits session.

After Charge / Stream / Embed

Read AiResponse::$credits, EmbeddingResponse::$credits, or StreamSummary::$credits (CreditsUsage):
  • charged — actual credits debited (charged.amount from API).
  • tokensTotal, tokensInput, tokensOutput — from the API response.
  • pricing — shared pricing object when present.
Display copy using CreditsPricing::footnote() when you need a static hint (e.g. “1 credit ≈ 1,000 tokens (min 1 credit per request)”).

Features catalog

Features.php lists feature_key, label, default_model, default_backend, sortnot fixed cost fields. Use it for routing and UI labels only.

Purchase / checkout

Product checkout URLs come from Products.php with redirect_to set to your TYPO3 backend return URL. Use checkout_url as returned — do not append extra Pabbly query parameters client-side. The client rewrites Pabbly custom fields named cf_credittoken* to the install Bearer token (from RuntimeSettingsService::getTokenPlain()), not the license key. Checkout opens in a TYPO3 backend modal iframe. The host must allow embedding (frame-ancestors / CSP) and its checkout API (e.g. /api/user/checksteps) must accept requests from the embedded checkout page. A 403 on that API is returned by t3planet.shop, not TYPO3 — fix on the shop/API side or use Open in new tab in the modal until embedding is supported. Configuration/ContentSecurityPolicies.php extends backend frame-src and connect-src for T3Planet/Pabbly hosts.

CLI, scheduler, and domain_mismatch

Every Charge / Stream / Embed body includes domain. The license server only accepts hostnames registered for that install. Backend HTTP requests use HTTP_HOST (unchanged). CLI and scheduler contexts have no web request; without a stored hostname the client previously fell back to localhost and the API returned domain_mismatch. CreditsDomainResolver (since v1.1+) resolves hostname in this order when HTTP_HOST is empty:
  1. credits_domain in tx_nst3af_runtime_setting (set at token activation and on the first backend request with a real host)
  2. $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyBaseUrl']
  3. First site configuration with an absolute base URL (https://host/)
  4. DDEV_PRIMARY_URL in DDEV containers
  5. Extension Configuration t3planetCreditsDomain (optional override)
After upgrading: open any backend module once (or re-activate Credits) so credits_domain is persisted, or set base: 'https://your-production-host/' in site config. Child extensions (t3cs, t3as, …) do not pass domain manually — inject AiServiceInterface and call embed() / complete() as in the backend.

Further reading