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.php—AiServiceInterface::complete()returnsAiResponseplusCreditsUsage.POST /API/AI/Stream.php—AiServiceInterface::stream()returns incrementalstringchunks; readStreamSummaryviagetReturn().POST /API/AI/Embed.php—AiServiceInterface::embed()returnsEmbeddingResponseplusCreditsUsage.POST /API/AI/Abort.php— called automatically when a stream is cancelled or ends without settlement (see Abort on cancel).
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_creditin the APIpricingobject).
CreditsUsage after the call for the actual debit.
Pre-submit estimate (child extensions)
Before callingcomplete(), 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:
CreditsEstimateService):
feature_key— extension-local key inAiOptions; normalized to composer catalog keys before the API call.meta_json— must includeprompt(and any fields your feature sends).endpoint—charge(default) orembed.
Feature key mapping (all extensions)
Child extensions keep their own telemetry keys inAiOptions::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:
CreditsFeatureKeyCatalog. Stream requests always bill as stream;
embed requests as embedding.
Streaming (Stream.php)
InjectAiServiceInterface only — routing is handled by T3PlanetCreditAiService and
ProxyAiExecutor.
SSE events (server → client)
token— Appenddata.deltato the UI buffer (incremental, not cumulative).ping— Ignore (keepalive).usage— Mandatory settlement; buildsStreamSummaryandCreditsUsage.done— Optional; the stream may end afterusageonly.
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::$requestUuidon 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 throwsCreditsApiException(no auto-retry). Wait for the first stream to finish and retry the same UUID, or use a new UUID for a parallel request.
inputs[] in meta_json for Stream.
Caller attribution (Charge / Stream / Embed / Estimate)
Always setextensionKey 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_uidwhen set.
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)
nst3af_credits_estimate and an active
credits session.
After Charge / Stream / Embed
ReadAiResponse::$credits, EmbeddingResponse::$credits, or
StreamSummary::$credits (CreditsUsage):
charged— actual credits debited (charged.amountfrom API).tokensTotal,tokensInput,tokensOutput— from the API response.pricing— sharedpricingobject when present.
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, sort — not fixed cost fields. Use it for
routing and UI labels only.
Purchase / checkout
Product checkout URLs come fromProducts.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:
credits_domainintx_nst3af_runtime_setting(set at token activation and on the first backend request with a real host)$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyBaseUrl']- First site configuration with an absolute
baseURL (https://host/) DDEV_PRIMARY_URLin DDEV containers- Extension Configuration
t3planetCreditsDomain(optional override)
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
- T3Planet Credits — product overview
- Privacy — T3Planet Credits — data sent when credits mode is on