PlatformXeDocs
Get API Key

AI

SDK methods for the product AI gateway — chat completion (with tool calling), structured extraction, knowledge retrieval, your AI profile and usage.

The client.ai namespace reaches the PlatformXe AI gateway (Gemini, Grok and Claude through Vercel AI Gateway, every request under zero data retention). Every call runs under the AI profile for (your API key's caller service, featureKey) — PlatformXe owns the profile's policy, provider and model allow-lists, size limits and daily caps; your app owns its prompts. See Product AI & Vision for how profiles, limits and quotas work. Available in TypeScript, Python, and Go SDKs.

SDKInstallNamespace
TypeScriptnpm install @caldera/platformxe-sdk (1.8.0+)client.ai
Pythonpip install platformxe (1.7.0+)client.ai, client.knowledge
Gogo get github.com/calderax/platformxe-go@v1.7.0client.Ai, client.Knowledge

The TypeScript client.knowledge namespace and tool-calling fields ship with the next TypeScript SDK release (types 3.6.0).

The ai.* methods require the ai:complete scope and share the 600 requests/hr per-key rate tier; the knowledge.* methods require ai:knowledge (1,200 requests/hr per key).

Chat completion

Run one chat turn. provider is gemini, grok or claude (claude from types 3.7.0); model optionally overrides the profile default and must be on both allow-lists. A caller that should not choose a provider itself can read allowedProviders from its profile (client.ai.profile) and send the first one — the xAdmin document converter does. A profile allowing more than 4,096 output tokens gets a 120 s gateway timeout instead of 55 s.

const reply = await client.ai.complete({
  featureKey: 'guest-concierge',
  provider: 'gemini',
  messages: [
    { role: 'system', content: 'You are the concierge for 12 Admiralty Way.' },
    { role: 'user', content: 'What time is check-out?' },
  ],
});

console.log(reply.content);           // "Check-out is at 11:00."
console.log(reply.model);             // "google/gemini-2.5-flash"
console.log(reply.usage?.totalTokens);
reply = client.ai.complete(
    feature_key="guest-concierge",
    provider="gemini",
    messages=[
        {"role": "system", "content": "You are the concierge for 12 Admiralty Way."},
        {"role": "user", "content": "What time is check-out?"},
    ],
)
print(reply["content"])
reply, err := client.Ai.Complete(platformxe.AiCompleteInput{
    FeatureKey: "guest-concierge",
    Provider:   "gemini",
    Messages: []platformxe.AiMessage{
        {Role: "system", Content: "You are the concierge for 12 Admiralty Way."},
        {Role: "user", Content: "What time is check-out?"},
    },
})
fmt.Println(reply.Content)

Structured extraction

Send prompts (and up to 4 images, when the profile allows images) and get a JSON object back, validated against your JSON Schema. modelTier is FAST or PREMIUM; temperature is 0–2 (default 0.2).

const result = await client.ai.extract({
  featureKey: 'document.freeform',
  systemPrompt: 'Extract the invoice fields.',
  userPrompt: invoiceText,
  images: [{ url: 'https://cdn.example.com/invoice.jpg' }],
  outputJsonSchema: {
    type: 'object',
    properties: { invoiceNumber: { type: 'string' }, total: { type: 'number' } },
    required: ['invoiceNumber', 'total'],
  },
  modelTier: 'FAST',
});

console.log(result.data.total);
result = client.ai.extract(
    feature_key="document.freeform",
    system_prompt="Extract the invoice fields.",
    user_prompt=invoice_text,
    images=[{"url": "https://cdn.example.com/invoice.jpg"}],
    output_json_schema={
        "type": "object",
        "properties": {"invoiceNumber": {"type": "string"}, "total": {"type": "number"}},
        "required": ["invoiceNumber", "total"],
    },
    model_tier="FAST",
)
print(result["data"]["total"])
temperature := 0.0
result, err := client.Ai.Extract(platformxe.AiExtractInput{
    FeatureKey:   "document.freeform",
    SystemPrompt: "Extract the invoice fields.",
    UserPrompt:   invoiceText,
    Images:       []platformxe.AiImageInput{{URL: "https://cdn.example.com/invoice.jpg"}},
    OutputJSONSchema: map[string]interface{}{
        "type":     "object",
        "required": []string{"invoiceNumber", "total"},
    },
    ModelTier:   "FAST",
    Temperature: &temperature, // a pointer, so 0 is sent
})
fmt.Println(result.Data["total"])

If the model answers in a shape that does not match outputJsonSchema, the call fails with 422 AI_OUTPUT_INVALID and error.details.path — the JSON Pointer of the first failing value. Read it from err.details.path (TypeScript), err.details["path"] (Python) or apiErr.Details["path"] (Go).

Tool calling

When your profile allows tools (allowTools), complete can offer the model up to 16 function tools. PlatformXe passes the tools through and returns the model's tool calls — it never executes a tool. Your app runs each call through its own permission-checked code, then sends the result back in the next turn: the assistant message carrying toolCalls, followed by one role: "tool" message per call with its toolCallId. A profile without allowTools refuses tools (and tool messages) with 400 BAD_REQUEST "tools not enabled for this feature".

  • tools[]: name (1–64 of A-Z a-z 0-9 _ . -, unique), optional description (≤ 1,024 characters), optional parameters (a JSON Schema object — a schema that does not compile is a 400).
  • toolChoice: "auto" (default), "none", "required", or { name } to force one tool.
  • The result adds toolCalls ([{ id, name, arguments }], arguments a JSON string) and finishReason (stop, length, tool_calls, content_filter, other); content is "" when the model only called tools.
  • Safety applies to tool traffic too: input screening and PII redaction cover tool results and earlier call arguments, and output screening covers every returned call's arguments.
const turn = await client.ai.complete({
  featureKey: 'ai-canvas',
  provider: 'gemini',
  messages: [{ role: 'user', content: 'How many leave days does Ada have left?' }],
  tools: [{
    name: 'hr.leave_balance',
    description: 'Remaining annual leave days for one employee.',
    parameters: { type: 'object', properties: { employeeRef: { type: 'string' } }, required: ['employeeRef'] },
  }],
});

if (turn.finishReason === 'tool_calls') {
  // Run each call yourself — permission checks and audit are yours.
  const results = await Promise.all(turn.toolCalls!.map(runToolWithPermissions));
  const answer = await client.ai.complete({
    featureKey: 'ai-canvas',
    provider: 'gemini',
    tools,
    messages: [
      ...previous,
      { role: 'assistant', content: '', toolCalls: turn.toolCalls },
      ...results.map((r) => ({ role: 'tool' as const, toolCallId: r.id, content: JSON.stringify(r.data) })),
    ],
  });
}
turn = client.ai.complete(
    feature_key="ai-canvas",
    provider="gemini",
    messages=[{"role": "user", "content": "How many leave days does Ada have left?"}],
    tools=[{"name": "hr.leave_balance", "parameters": {"type": "object", "properties": {"employeeRef": {"type": "string"}}}}],
    tool_choice="auto",
)
for call in turn.get("toolCalls", []):
    ...  # run it, then send {"role": "tool", "toolCallId": call["id"], "content": ...}
turn, err := client.Ai.Complete(platformxe.AiCompleteInput{
    FeatureKey: "ai-canvas",
    Provider:   "gemini",
    Messages:   []platformxe.AiMessage{{Role: "user", Content: "How many leave days does Ada have left?"}},
    Tools:      []platformxe.AiToolDefinition{{Name: "hr.leave_balance"}},
    ToolChoice: "auto",
})
// turn.ToolCalls, turn.FinishReason

Knowledge retrieval

The client.knowledge namespace indexes your documents into PlatformXe and searches them, so answers can cite your own data. It uses its own scope, ai:knowledge (1,200 requests/hr per key). Documents belong to your API key's caller service; another service never sees them.

MethodRoute
knowledge.upsertDocument / upsert_document / UpsertDocumentPUT /api/v1/ai/knowledge/documents
knowledge.deleteDocument / delete_document / DeleteDocumentDELETE /api/v1/ai/knowledge/documents?collection=&sourceRef=
knowledge.listDocuments / list_documents / ListDocumentsGET /api/v1/ai/knowledge/documents?collection=&cursor=&limit=
knowledge.search / search / SearchPOST /api/v1/ai/knowledge/search

Indexing. upsertDocument takes featureKey (the AI profile the embedding spend is metered against), collection, your own sourceRef, title, optional url and acl, and text (markdown or plain, ≤ 400,000 characters). The text is chunked by heading (~800 tokens, 100-token overlap, code fences never split); a re-upsert with the same text changes metadata only (changed: false). pendingEmbeddings counts chunks still waiting for an embedding — search works lexically meanwhile.

Text is stored as you send it. PlatformXe applies no PII redaction at index time — never index Tier-1 / Tier-2 personal data. Documents live until you delete them; when a source is removed on your side, call deleteDocument. listDocuments (compare contentHash) lets you reconcile.

ACLs. acl is a list of opaque tags you own (for example role:hr). A search sees a document when its aclTags hold any of the document's tags; a document with no tags is visible to every search of your service.

Search. Postgres full-text and trigram matching pick the candidates; when embeddings are available the query is embedded and cosine similarity is fused with the lexical ranks (reciprocal rank fusion) — mode: "hybrid", otherwise "lexical". Each hit carries sourceRef, ordinal and heading for citations. The query is screened like any AI input (403 AI_POLICY_DENIED).

await client.knowledge.upsertDocument({
  featureKey: 'ai-canvas',
  collection: 'hr-policies',
  sourceRef: 'policy/annual-leave',
  title: 'Annual leave policy',
  acl: ['role:hr'],
  text: '# Annual leave\n\nEvery full-time employee earns 20 working days a year.',
});

const { mode, hits } = await client.knowledge.search({
  featureKey: 'ai-canvas',
  collections: ['hr-policies'],
  query: 'how many days of annual leave do I get',
  aclTags: ['role:hr'],
  k: 5,
});
hits.forEach((h) => console.log(`${h.title} [${h.sourceRef}#${h.ordinal}]`, h.text));
client.knowledge.upsert_document(
    feature_key="ai-canvas", collection="hr-policies", source_ref="policy/annual-leave",
    title="Annual leave policy", text="# Annual leave\n\n20 working days a year.", acl=["role:hr"],
)
result = client.knowledge.search(
    feature_key="ai-canvas", collections=["hr-policies"], query="annual leave", acl_tags=["role:hr"], k=5,
)
page = client.knowledge.list_documents("hr-policies")          # {items, nextCursor}
client.knowledge.delete_document("hr-policies", "policy/annual-leave")
_, err := client.Knowledge.UpsertDocument(platformxe.KnowledgeUpsertDocumentInput{
    FeatureKey: "ai-canvas", Collection: "hr-policies", SourceRef: "policy/annual-leave",
    Title: "Annual leave policy", ACL: []string{"role:hr"}, Text: "# Annual leave\n\n20 working days a year.",
})
res, err := client.Knowledge.Search(platformxe.KnowledgeSearchInput{
    FeatureKey: "ai-canvas", Collections: []string{"hr-policies"}, Query: "annual leave", ACLTags: []string{"role:hr"}, K: 5,
})

Your AI profile

Read the calling service's own profile for one feature: status, allow-lists, limits and today's (UTC) usage. The policy text is withheld.

const profile = await client.ai.profile('guest-concierge');
console.log(profile.status, profile.dailyTokenCap, profile.usedTokensToday, profile.resetsAt);
profile = client.ai.profile("guest-concierge")
profile, err := client.Ai.Profile("guest-concierge")

Usage

The calling service's own AI and vision usage, grouped by feature, kind (ai / vision / embedding), provider and model. from / to are ISO-8601 — default the last 30 days, at most 92 days.

const usage = await client.ai.usage({ from: '2026-09-01T00:00:00Z', featureKey: 'guest-concierge' });
console.log(usage.totals.requests, usage.totals.costCredits);
usage = client.ai.usage(from_="2026-09-01T00:00:00Z", feature_key="guest-concierge")
usage, err := client.Ai.Usage(platformxe.AiUsageQuery{From: "2026-09-01T00:00:00Z", FeatureKey: "guest-concierge"})

Errors

CodeStatusMeaning
BAD_REQUEST400Malformed body, or a profile / hard limit breached
AI_PROFILE_NOT_FOUND403No profile for your caller service + featureKey
AI_PROFILE_DISABLED403The profile is switched off
AI_POLICY_DENIED403The input was refused by the safety screen
AI_OUTPUT_BLOCKED422The model reply was refused by output screening
AI_OUTPUT_INVALID422extract only — off-schema reply (details.path)
AI_QUOTA_EXCEEDED429The profile's daily cap is reached — Retry-After runs to the next UTC midnight
AI_UPSTREAM_ERROR502The model provider failed or timed out
AI_NOT_CONFIGURED503The gateway is not configured on that deployment
SERVER_ERROR500Unexpected failure

AI_QUOTA_EXCEEDED is never retried by the SDKs: in TypeScript it is a RateLimitError whose code is AI_QUOTA_EXCEEDED and whose retryAfter is the seconds to midnight; Python sets err.retry_after; Go sets apiErr.RetryAfter. A per-key 429 RATE_LIMITED is still retried by the TypeScript and Go clients.

Scopes required

MethodScopeRate limit
ai.complete()ai:complete600/hr
ai.extract()ai:complete600/hr
ai.profile()ai:complete600/hr
ai.usage()ai:complete600/hr
knowledge.upsertDocument() / deleteDocument() / listDocuments() / search()ai:knowledge1,200/hr