@capsiynau/intelligence-client
v0.1.1
Published
Typed client for the Intelligence Layer API. Auth, tenant context, idempotency, timeouts, retry on safe operations, error normalisation, job polling. No intelligence implementation.
Readme
@capsiynau/intelligence-client
The typed client products use to reach the Intelligence Layer.
Private, unpublished, and not publishable. Capsiynau, Nodiadau and Postia consume it from the repo; nothing here reaches a public registry (ADR 0028).
What it is
The transport concerns every caller would otherwise get right five times, slightly differently:
- service-credential auth and opaque tenant/user context
- idempotency keys, generated when you do not supply one
- per-attempt timeouts, with caller aborts kept distinct from them
- retry on safe operations only
- one typed error hierarchy, from either wire shape
- request-ID propagation, so one trace spans product and platform
- job polling that ends on any terminal status the contract defines
What it is not
The intelligence. There are no prompts here, no provider names, no model
routing, no segmentation rules, no Welsh handling. Someone holding this package
and @capsiynau/intelligence-contracts can call the platform and cannot rebuild
it (ADR 0006). scripts/check-boundary-leaks.mjs enforces that on every commit
rather than trusting it.
Server-side only
The credential is a service credential (ADR 0002). It does not belong in browser JavaScript, and nothing in this package can stop a bundler that is pointed at it
- that rule lives in review and in the boundary guard.
Use
import { IntelligenceClient } from '@capsiynau/intelligence-client'
const intelligence = new IntelligenceClient({
credential: process.env.INTELLIGENCE_API_KEY,
baseUrl: process.env.INTELLIGENCE_API_URL,
tenant: organisationReference, // opaque; the platform stores no product identity
})
// Submit and wait.
const result = await intelligence.run('transcription', {
asset_id: assetId,
source_language: 'cy',
quality_profile: 'welsh-broadcast',
})
// Or drive the lifecycle yourself.
const job = await intelligence.createJob('transcription', { asset_id: assetId })
const done = await intelligence.waitForJob('transcription', job.job_id, {
onProgress: (j) => activityStrip.update(j.status),
})Quality profiles are named intents, never models. Which model serves
welsh-broadcast is the platform's business and may change without a client
release (ADR 0007).
Errors
Every failure arrives as an IntelligenceError subclass carrying code,
status, requestId, details and retryable. Branch on the class or on the
code, never on the message.
import { QuotaError, RateLimitError } from '@capsiynau/intelligence-client'
try {
await intelligence.run('translation', payload)
} catch (err) {
if (err instanceof QuotaError) return showUpgradePrompt()
if (err instanceof RateLimitError) return retryLater(err.details.retry_after)
log.error({ code: err.code, requestId: err.requestId }, 'intelligence call failed')
throw err
}The client understands both the v1 envelope and the legacy { error, code }
body, so it works against the platform before and after the envelope rollout
(ADR 0017).
Retry rule
A GET or HEAD may be retried. A POST may be retried only when it
carried an idempotency key, because that key is what makes the second attempt
free rather than a second charge (ADR 0013). The failure must also be retryable
- a validation error does not improve by being sent again.
The client generates a key for every job submission, so submissions are retried
by default. client.request('POST', ...) without a key is sent exactly once.
Cancellation
cancelJob is cooperative and best-effort (ADR 0016). It records the intent; it
does not promise the work stopped, and anything already inside a provider call
may still be billed.
Testing
npx vitest run packages/intelligence-clientThe suite runs against a real node:http fixture server rather than a mocked
fetch, so headers, status codes, timeouts and aborts are exercised as they
will be in production. It runs in the node environment, not the repo's default
jsdom - jsdom's AbortController is not the one Node's fetch accepts.
