# SPDX-License-Identifier: Apache-2.0 # Copyright 2026 Craton Software Company # # OpenAPI 3.1 description of the Craton TensorWasm HTTP API gateway. # # This file is the canonical machine-readable contract for the # tensor-wasm-api router defined in crates/tensor-wasm-api/src/server.rs and # handler set in crates/tensor-wasm-api/src/routes.rs. The # `openapi_validation_test` integration test (under the same crate's tests/ # directory) asserts that every route registered on the live axum router # has a corresponding `paths` entry below and that the request body schema # for `POST /functions` round-trips a real `CreateFunctionRequest` Rust # value. CI runs the test on every PR; see .github/workflows/ci.yml job # `openapi`. # # Indentation is two spaces. Top-level keys (openapi, info, servers, …) # sit at column 0; the `paths:` map's keys (e.g. `/healthz:`) sit at # column 2 so the test's structural scan can pick them up. Do not # introduce tab characters or alternative indentation widths without # updating the parser in openapi_validation_test.rs. openapi: 3.1.0 info: title: TensorWasm HTTP API description: | REST surface of a Craton TensorWasm serverless Wasm node. Prose documentation, error semantics, and the full middleware stack live in crates/tensor-wasm-api/API.md. version: 0.3.7 license: name: Apache-2.0 identifier: Apache-2.0 contact: name: Craton Software Company email: security@craton.com.ar url: https://github.com/craton-co/craton-tensor-wasm servers: - url: http://localhost:8080 description: Local development server security: - BearerAuth: [] tags: - name: lifecycle description: Function deploy / delete - name: invoke description: Synchronous and asynchronous invocations - name: jobs description: Async-job poll API - name: ops description: Operator-facing endpoints (health, metrics) - name: kernels description: | Kernel registry (roadmap feature #3, B6.4). The routes under this tag are only mounted when the gateway binary is built with `--features kernel-registry-api`; on the default build they are absent and a request to them 404s at the router. When the feature is on but `TENSOR_WASM_API_KERNEL_HMAC_KEY` is unset the handlers return `503 kernel_registry_not_configured`. See crates/tensor-wasm-api/src/kernels.rs and docs/KERNEL-REGISTRY.md. - name: openai-compat description: | OpenAI-compatible inference gateway (T41, v0.4). The two routes under this tag accept the public OpenAI REST request shapes so off-the-shelf SDKs (Python `openai`, Node `openai`, LangChain, LlamaIndex, ...) can target the gateway URL without modification. The handlers resolve the request's `model` field against an env-configured `model -> function_uuid` map (`TENSOR_WASM_API_OPENAI_MODEL_MAP`) and dispatch through the shared executor. See `docs/OPENAI-COMPAT.md` for the wire contract and the operator configuration knob. - name: snapshot description: | Snapshot save / restore (M5). `/snapshot/save` captures a deployed function into an HMAC-SHA256-signed snapshot blob and returns it; `/snapshot/restore` verifies the signature and decodes the blob's provenance. Both routes consume the operator-configured `TENSOR_WASM_API_SNAPSHOT_HMAC_KEY`; when it is unset they return `503 snapshot_signing_not_configured`. The routes sit on the protected stack (bearer auth + per-tenant scope + per-tenant ownership). See crates/tensor-wasm-api/src/routes.rs and crates/tensor-wasm-api/src/config.rs. paths: /healthz: get: tags: [ops] summary: Liveness probe operationId: healthz security: [] responses: "200": description: Service is alive headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/Health" /metrics: get: tags: [ops] summary: Prometheus text exposition operationId: metrics description: | Open by default so Prometheus scrapers and k8s tooling can hit it without a bearer token. Operators may opt into a bearer-token gate by setting `TENSOR_WASM_API_METRICS_TOKEN`; when that env var is set the endpoint requires `Authorization: Bearer ` and returns `401` otherwise. security: [] responses: "200": description: Prometheus 0.0.4 text exposition headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: text/plain: schema: type: string "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/RateLimited" /functions: post: tags: [lifecycle] summary: Deploy a new Wasm module operationId: createFunction parameters: - $ref: "#/components/parameters/TenantHeader" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateFunctionRequest" responses: "200": description: Function deployed headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/CreateFunctionResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" /functions/{id}: delete: tags: [lifecycle] summary: Delete a deployed function operationId: deleteFunction parameters: - $ref: "#/components/parameters/FunctionId" - $ref: "#/components/parameters/TenantHeader" responses: "204": description: Function removed headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" /functions/{id}/invoke: post: tags: [invoke] summary: Invoke a function synchronously operationId: invokeFunction parameters: - $ref: "#/components/parameters/FunctionId" - $ref: "#/components/parameters/TenantHeader" requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/InvokeRequest" responses: "200": description: Invocation complete headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/InvokeResult" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" "504": $ref: "#/components/responses/InvokeTimeout" /functions/{id}/invoke-async: post: tags: [invoke] summary: Invoke a function asynchronously operationId: invokeFunctionAsync parameters: - $ref: "#/components/parameters/FunctionId" - $ref: "#/components/parameters/TenantHeader" requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/InvokeRequest" responses: "202": description: Job accepted headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/InvokeAsyncResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" /functions/{id}/invoke-stream: post: tags: [invoke] summary: Invoke a function and stream the response (SSE or chunked). description: | Streaming counterpart of `/functions/{id}/invoke`. The response shape is selected from the request's `Accept` header: * `Accept: text/event-stream` — Server-Sent Events. Each chunk the guest emits via the `wasi:tensor/host.emit-chunk` host function is rendered as one `event: chunk` frame. The stream terminates with an `event: done` frame on success or an `event: error` frame on guest failure / deadline-elapsed. * Otherwise — `Content-Type: application/octet-stream`, `Transfer-Encoding: chunked`. Each guest chunk is forwarded verbatim as one HTTP chunk frame, followed by the same terminal `event: done` / `event: error` line so clients on either negotiation outcome detect end-of-stream uniformly. ### v0.4 wiring (T34) The route is wired end-to-end through `tensor_wasm_wasi_gpu::StreamingContext`. Guest emits land on a `tokio::sync::mpsc::Receiver>` the gateway drains into the response body. The cooperative-deadline path (T36) routes a deadline-elapsed signal into a final `event: error` with `{"reason":"deadline_elapsed"}` so SSE clients can distinguish it from a generic trap. See `docs/STREAMING.md`. operationId: invokeFunctionStream parameters: - $ref: "#/components/parameters/FunctionId" - $ref: "#/components/parameters/TenantHeader" - name: Accept in: header required: false description: | Set to `text/event-stream` to receive SSE framing. Any other value (or absence) selects the `application/octet-stream` chunked-transfer branch. schema: type: string requestBody: required: false content: application/json: schema: type: object description: | Arbitrary JSON payload, same shape as the synchronous invoke endpoint. The v0.x executor ignores the value. responses: "200": description: | Streaming response. The body framing depends on the negotiated content type — see the operation summary. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: text/event-stream: schema: type: string description: | Server-Sent Events stream. Each emitted chunk is one `event: chunk` frame; the stream ends with an `event: done` frame on success or an `event: error` frame on failure / deadline-elapsed. application/octet-stream: schema: type: string format: binary description: | Raw chunked-transfer bytes — one HTTP chunk per guest-emitted chunk, followed by a final terminal `event: done` / `event: error` line. "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" /v1/completions: post: tags: [openai-compat] summary: OpenAI-compatible text completions (T41 wired) description: | Accepts the public OpenAI `POST /v1/completions` request shape and dispatches to the deployed function the gateway operator has aliased to the requested `model` (via `TENSOR_WASM_API_OPENAI_MODEL_MAP`). Returns the standard OpenAI `text_completion` envelope on success, or `404 model_not_found` when the model alias is unknown. When `stream: true`, returns `text/event-stream` with one `data: { ... }` SSE frame per emitted chunk and a terminal `data: [DONE]\n\n` line. The `X-TensorWasm-Tenant` header is not honoured on this route, because OpenAI SDKs never send it. Tenant resolution comes from the bearer token's scope (see `docs/OPENAI-COMPAT.md`). operationId: openaiCompletions requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CompletionsRequest" responses: "200": description: Completion result (or SSE stream when `stream:true`). headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/CompletionsResponse" text/event-stream: schema: type: string description: | SSE stream. Each guest chunk lands as one `data: { ... }` frame whose JSON carries the OpenAI delta shape; the stream terminates with a literal `data: [DONE]\n\n` line. "400": description: Malformed request body (OpenAI envelope). content: application/json: schema: $ref: "#/components/schemas/OpenAiError" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: | The requested `model` is not configured in the gateway's `TENSOR_WASM_API_OPENAI_MODEL_MAP`. Carries OpenAI envelope `code: "model_not_found"`. content: application/json: schema: $ref: "#/components/schemas/OpenAiError" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" /v1/chat/completions: post: tags: [openai-compat] summary: OpenAI-compatible chat completions (T41 wired) description: | Accepts the public OpenAI `POST /v1/chat/completions` request shape (system / user / assistant `messages` array) and dispatches to the function the operator has aliased to the requested `model`. The `messages` array is concatenated into a single role-tagged prompt string before dispatch (`system: ...\nuser: ...\nassistant:`). Returns the standard OpenAI `chat.completion` envelope on success, or `404 model_not_found` when the alias is unknown. Streaming framing matches `/v1/completions`, except the JSON delta uses the chat shape (`choices[0].delta.content`) and the object kind is `chat.completion.chunk`. operationId: openaiChatCompletions requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ChatCompletionsRequest" responses: "200": description: Chat completion (or SSE stream when `stream:true`). headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ChatCompletionsResponse" text/event-stream: schema: type: string description: | SSE stream. Each guest chunk lands as one `data: { ... }` frame whose JSON carries the OpenAI chat delta shape; the stream terminates with a literal `data: [DONE]\n\n` line. "400": description: Malformed request body (OpenAI envelope). content: application/json: schema: $ref: "#/components/schemas/OpenAiError" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: | The requested `model` is not configured in the gateway's `TENSOR_WASM_API_OPENAI_MODEL_MAP`. Carries OpenAI envelope `code: "model_not_found"`. content: application/json: schema: $ref: "#/components/schemas/OpenAiError" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" /jobs/{id}: get: tags: [jobs] summary: Poll an async-invocation job operationId: getJob parameters: - name: id in: path required: true schema: type: string format: uuid responses: "200": description: Current job status headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/JobRecord" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" /snapshot/save: post: tags: [snapshot] summary: Capture a deployed function into a signed snapshot blob description: | Captures the deployed Wasm module bytes of the named function into a zstd-compressed, HMAC-SHA256-signed snapshot blob and returns it base64-encoded. The blob round-trips through `POST /snapshot/restore`. Authorization: protected stack (bearer auth + tenant scope). The handler runs `authorize_tenant` against the bearer token's scope (an out-of-scope token gets `403 tenant_scope_denied` before any lookup) and then a per-resource owner check against the function's `tenant_id` (a wildcard-scoped caller from another tenant cannot snapshot tenant A's function). Signing uses `TENSOR_WASM_API_SNAPSHOT_HMAC_KEY`; when it is unset the route returns `503 snapshot_signing_not_configured` rather than emitting an unsigned blob. NOTE: this captures the function's *deployed module bytes* — the working save/restore-of-bytes layer with end-to-end HMAC signing. Capturing a *live running instance's* linear / GPU memory needs an executor capture hook that does not exist yet; requesting that capability surfaces `501 not_implemented`. See crates/tensor-wasm-api/src/routes.rs. operationId: snapshotSave parameters: - $ref: "#/components/parameters/TenantHeader" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SnapshotSaveRequest" responses: "200": description: Snapshot captured and signed; blob returned base64. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/SnapshotSaveResponse" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" "501": $ref: "#/components/responses/NotImplemented" "503": $ref: "#/components/responses/SnapshotSigningUnavailable" /snapshot/restore: post: tags: [snapshot] summary: Verify and decode a signed snapshot blob description: | Accepts a base64-encoded snapshot blob previously produced by `POST /snapshot/save`, verifies its HMAC-SHA256 signature against `TENSOR_WASM_API_SNAPSHOT_HMAC_KEY`, and returns the HMAC-authenticated provenance (captured tenant, instance id, uncompressed size, wire version). Hardening: the reader always requires a signature (`require_signature`) so a stripped-trailer downgrade to an unsigned v2 blob is refused; `TENSOR_WASM_API_SNAPSHOT_REQUIRE_SIGNATURE` is the operator surface for this posture. A 256 MiB decompression cap bounds restore-time memory pressure. Authorization: protected stack (bearer auth + tenant scope). After HMAC verification recovers the snapshot's captured tenant, the handler enforces that it equals the caller's resolved tenant — a cross-tenant restore is `403 tenant_scope_denied`. A wrong / missing key or a tampered blob is `403 snapshot_signature_invalid` (a single opaque rejection, not a decode oracle). NOTE: this verifies and decodes the snapshot *envelope*. Reconstituting a *live running instance* from the captured memory needs an executor restore hook that does not exist yet; requesting that capability surfaces `501 not_implemented`. operationId: snapshotRestore parameters: - $ref: "#/components/parameters/TenantHeader" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SnapshotRestoreRequest" responses: "200": description: Snapshot signature verified; provenance returned. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/SnapshotRestoreResponse" "400": description: | `invalid_json` (malformed request body) or `invalid_base64` (the `snapshot_b64` field is not valid base64). Carries the native `{error:{kind,message}}` envelope. content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" "401": $ref: "#/components/responses/Unauthorized" "403": description: | `tenant_scope_denied` (the bearer token is not scoped to the requested tenant, or the snapshot was captured for a different tenant) or `snapshot_signature_invalid` (HMAC verification failed, the blob is unsigned, or it is malformed). Carries the native `{error:{kind,message}}` envelope. content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" "501": $ref: "#/components/responses/NotImplemented" "503": $ref: "#/components/responses/SnapshotSigningUnavailable" /kernels: post: tags: [kernels] summary: Publish a signed kernel manifest + PTX text description: | FEATURE-GATED: this route is only mounted when the gateway is built with `--features kernel-registry-api`; the default build does not register it (request 404s at the router). When the feature is on but `TENSOR_WASM_API_KERNEL_HMAC_KEY` is unset the handler returns `503 kernel_registry_not_configured`. Publishes a signed `KernelManifest` plus its PTX source. The registry re-verifies that `BLAKE3(ptx_text)` matches `manifest.digest` and that the manifest's HMAC-SHA256 signature verifies under the server's configured key before persisting. Authorization is stricter than the other routes: in addition to `bearer_auth` + `tenant_scope`, the caller's bearer token must appear in the `TENSOR_WASM_API_KERNEL_PUBLISH_TOKENS` allowlist (the **kernel-publish** scope). Dev mode (empty `TENSOR_WASM_API_TOKENS`) rejects every publish with `403 kernel_publish_disabled_in_dev_mode`. See crates/tensor-wasm-api/src/kernels.rs. operationId: publishKernel parameters: - $ref: "#/components/parameters/TenantHeader" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PublishKernelRequest" responses: "201": description: Kernel published; canonical name/version echoed back. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/PublishKernelResponse" "400": description: | `digest_mismatch` (BLAKE3 of `ptx_text` did not match `manifest.digest`) or `invalid_request` (other registry rejection). Carries the native `{error:{kind,message}}` envelope. content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" "401": $ref: "#/components/responses/Unauthorized" "403": description: | `kernel_publish_disabled_in_dev_mode` (gateway in dev mode), `kernel_publish_scope_required` (token not in `TENSOR_WASM_API_KERNEL_PUBLISH_TOKENS`), `bad_signature` (HMAC verification failed), or `publisher_not_allowed` (manifest publisher not in the registry allowlist). content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" "409": description: | `already_registered` — a manifest with the same `name@version` is already present. content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" "413": $ref: "#/components/responses/PayloadTooLarge" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/KernelRegistryUnavailable" get: tags: [kernels] summary: List registered kernel manifests (PTX omitted) description: | FEATURE-GATED: only mounted under `--features kernel-registry-api`. Returns `503 kernel_registry_not_configured` when the feature is on but `TENSOR_WASM_API_KERNEL_HMAC_KEY` is unset. Lists manifests in unspecified order; PTX text is omitted (use `GET /kernels/{name}/{version}` to fetch source). Any authenticated tenant may list. `limit` is clamped to 1000 server-side; the response echoes the effective `offset` / `limit`. operationId: listKernels parameters: - $ref: "#/components/parameters/TenantHeader" - name: offset in: query required: false description: Index of the first manifest to return; defaults to 0. schema: type: integer minimum: 0 - name: limit in: query required: false description: | Maximum manifests in the returned page; defaults to 100, clamped to 1000 server-side. schema: type: integer minimum: 1 responses: "200": description: Page of manifests with the effective pagination echoed. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ListKernelsResponse" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/RateLimited" "503": $ref: "#/components/responses/KernelRegistryUnavailable" /kernels/{name}/{version}: get: tags: [kernels] summary: Resolve a kernel manifest + PTX text description: | FEATURE-GATED: only mounted under `--features kernel-registry-api`. Returns `503 kernel_registry_not_configured` when the feature is on but `TENSOR_WASM_API_KERNEL_HMAC_KEY` is unset. Resolves a single manifest plus its PTX source by `name`/`version`. Any authenticated tenant may resolve. Path segments are accepted verbatim; clients must percent-encode any reserved characters in `name` or `version`. operationId: resolveKernel parameters: - name: name in: path required: true description: Kernel name segment. schema: type: string - name: version in: path required: true description: Kernel version segment. schema: type: string - $ref: "#/components/parameters/TenantHeader" responses: "200": description: Resolved manifest and its PTX text. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ResolveKernelResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" "503": $ref: "#/components/responses/KernelRegistryUnavailable" components: securitySchemes: BearerAuth: type: http scheme: bearer description: | Bearer token from the TENSOR_WASM_API_TOKENS allowlist. Each entry may carry a `:tenant=...` clause restricting the token to a subset of tenants; see API.md `Per-tenant scopes`. An empty allowlist puts the gateway in dev mode (auth disabled). parameters: FunctionId: name: id in: path required: true description: Server-assigned function identifier (UUIDv4). schema: type: string format: uuid TenantHeader: name: X-TensorWasm-Tenant in: header required: false description: | Tenant scope (u64). Defaults to 0 if absent. Mandatory when the gateway runs with `TENSOR_WASM_API_REQUIRE_TENANT=1`. schema: type: integer format: int64 minimum: 0 headers: XRequestId: description: | Per-request UUID stamped by the audit middleware. Operators can correlate audit records, structured logs, and trace events by this id. schema: type: string format: uuid XTraceId: description: | W3C `trace-id` (32 lowercase hex chars) of the current span. Empty string if no trace context is active. schema: type: string RetryAfter: description: | Per RFC 9110 §10.2.3, integer seconds until the next request is likely to succeed. Always at least 1. schema: type: integer minimum: 1 responses: BadRequest: description: Validation failure headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" Unauthorized: description: Missing or unrecognised bearer token headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" Forbidden: description: Bearer token is not scoped to the requested tenant headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" NotFound: description: Function or job id is unknown headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" PayloadTooLarge: description: | Request body exceeded the 64 MiB cap enforced by axum's `DefaultBodyLimit::max`. Often rendered as a bare 413 with no body when the limit is hit during streaming. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" RateLimited: description: | Per-token QPS + burst exceeded. The `Retry-After` header carries the wait in integer seconds (RFC 9110 §10.2.3). headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" Retry-After: $ref: "#/components/headers/RetryAfter" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" InternalError: description: Underlying wasmtime or host failure headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" InvokeTimeout: description: Invocation exceeded its per-call deadline headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" KernelRegistryUnavailable: description: | Kernel registry routes are mounted but the backing registry is not configured: `TENSOR_WASM_API_KERNEL_HMAC_KEY` is unset, so the handler returns `503 kernel_registry_not_configured`. (The publish path can also surface `kernel_registry_storage_error` here on a backend I/O failure.) headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" NotImplemented: description: | `not_implemented` — the requested capability needs executor support that is not wired yet (full live-instance snapshot / restore of linear + GPU memory). The HMAC envelope layer of the `/snapshot/*` routes is fully wired; only the live-instance reconstitution returns this. Clients should NOT retry. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" SnapshotSigningUnavailable: description: | `snapshot_signing_not_configured` — the `/snapshot/*` routes are mounted but `TENSOR_WASM_API_SNAPSHOT_HMAC_KEY` is unset, so the gateway has no key to sign or verify snapshot blobs. The failure mode is configuration, not load; a client should NOT retry but surface the error to an operator who can set the key. headers: X-Request-Id: $ref: "#/components/headers/XRequestId" X-Trace-Id: $ref: "#/components/headers/XTraceId" content: application/json: schema: $ref: "#/components/schemas/ApiErrorEnvelope" schemas: FunctionId: type: string format: uuid description: Server-assigned function identifier (UUIDv4). JobId: type: string format: uuid description: Server-assigned job identifier (UUIDv4). Health: type: object properties: status: type: string enum: [ok] required: [status] additionalProperties: false CreateFunctionRequest: type: object description: | Body of `POST /functions`. Mirrors the `tensor_wasm_api::routes::CreateFunctionRequest` Rust struct. properties: name: type: string minLength: 1 description: Tenant-supplied display name (free-form, non-empty). wasm_b64: type: string minLength: 12 description: | Base64-encoded Wasm module bytes (standard alphabet, padded). Decoded value must validate as a complete Wasm module via `wasmparser::validate`. required: [name, wasm_b64] additionalProperties: false CreateFunctionResponse: type: object properties: id: $ref: "#/components/schemas/FunctionId" required: [id] additionalProperties: false InvokeRequest: type: object description: | Body of POST /functions/{id}/invoke (and the async sibling). Both fields are optional — an empty body or `{}` is the canonical no-args case, matching the pre-args wire contract. Unknown top-level fields are ignored for forward compatibility; malformed JSON surfaces as 400 invalid_json. properties: export: type: [string, "null"] description: | Optional export-name override. When omitted (or null), the server tries `_start` first and falls back to `main`, matching the WASI command convention. args: type: array default: [] description: | Argument list forwarded to the executor's `call_export_with_args` path. Each element is converted to the closest-fitting wasm value type: integers in [-2^31, 2^31) become i32, larger integers become i64, non-integer numerics become f64. Strings / arrays / null / booleans are rejected as 400 invalid_args. f32 cannot be selected from JSON unambiguously — write a guest wrapper that demotes from f64 if you need it. items: oneOf: - type: integer format: int64 - type: number format: double additionalProperties: false InvokeResult: type: object properties: function_id: $ref: "#/components/schemas/FunctionId" result: description: | Invocation result. For exports returning `()` this collapses to the literal string `"ok"` (legacy back-compat envelope). For exports with a non-empty result list this is a JSON array of return values — one element per wasm result slot, with i32/i64 rendered as JSON integers and f32/f64 as JSON numbers. required: [function_id, result] additionalProperties: false InvokeAsyncResponse: type: object properties: job_id: $ref: "#/components/schemas/JobId" required: [job_id] additionalProperties: false JobStatus: type: string enum: [pending, completed, failed] description: | Lifecycle state of an async invocation. `pending` jobs have no result yet; `completed` and `failed` jobs carry a `result` payload (the InvokeResult shape and the ApiErrorBody shape, respectively). JobRecord: type: object properties: id: $ref: "#/components/schemas/JobId" function_id: $ref: "#/components/schemas/FunctionId" status: $ref: "#/components/schemas/JobStatus" result: description: | Present once `status` transitions out of `pending`. For `completed` jobs this is the InvokeResult JSON object; for `failed` jobs it is the ApiErrorBody JSON object. created_unix_ms: type: integer format: int64 minimum: 0 required: [id, function_id, status, created_unix_ms] additionalProperties: false ApiErrorBody: type: object properties: kind: type: string description: Stable machine-readable identifier. enum: - invalid_json - invalid_name - invalid_base64 - invalid_wasm - invalid_args - missing_export - missing_tenant - unauthorized - tenant_scope_denied - not_found - instance_not_found - body_too_large - rate_limited - invoke_timeout - wasmtime - internal # Kernel registry (feature kernel-registry-api). - kernel_registry_not_configured - kernel_registry_storage_error - kernel_publish_disabled_in_dev_mode - kernel_publish_scope_required - publisher_not_allowed - bad_signature - digest_mismatch - already_registered - invalid_request # Snapshot save / restore (M5). - not_implemented - snapshot_signing_not_configured - snapshot_signature_invalid # Executor admission / resource limits (mapped from ExecError). # `capacity_exhausted` (503) is the engine-wide instance ceiling; # `tenant_capacity_exhausted` (429) is a single tenant over its # per-tenant fairness cap (max_instances_per_tenant) — distinct # retry semantics (reduce your own concurrency vs. wait for global # load to drop). - capacity_exhausted - tenant_capacity_exhausted - module_too_large - module_memory_too_large - epoch_ticker_not_running message: type: string description: Human-readable description (not part of the contract). required: [kind, message] additionalProperties: false ApiErrorEnvelope: type: object properties: error: $ref: "#/components/schemas/ApiErrorBody" required: [error] additionalProperties: false # ---- Snapshot save / restore shapes (M5) ------------------------------ SnapshotSaveRequest: type: object description: | Body of `POST /snapshot/save`. Mirrors the `tensor_wasm_api::routes::SnapshotSaveRequest` Rust struct. properties: function_id: $ref: "#/components/schemas/FunctionId" required: [function_id] additionalProperties: false SnapshotSaveResponse: type: object description: | Body of a successful `POST /snapshot/save`. Carries the full HMAC-SHA256-signed snapshot blob, base64-encoded. properties: function_id: $ref: "#/components/schemas/FunctionId" snapshot_b64: type: string description: | Base64-encoded signed snapshot blob (standard alphabet, padded). The exact bytes to hand back to `POST /snapshot/restore`; the HMAC trailer is part of these bytes. signed: type: boolean description: | Whether the blob carries an HMAC signature. Always `true` on this path — the route is only reachable with a key configured. required: [function_id, snapshot_b64, signed] additionalProperties: false SnapshotRestoreRequest: type: object description: | Body of `POST /snapshot/restore`. Mirrors the `tensor_wasm_api::routes::SnapshotRestoreRequest` Rust struct. properties: snapshot_b64: type: string minLength: 1 description: | Base64-encoded signed snapshot blob (standard alphabet, padded) previously returned by `POST /snapshot/save`. required: [snapshot_b64] additionalProperties: false SnapshotRestoreResponse: type: object description: | Body of a successful `POST /snapshot/restore`. Reports the HMAC-authenticated provenance recovered from the snapshot metadata. properties: tenant_id: type: integer format: int64 minimum: 0 description: Tenant the snapshot was originally captured for. instance_id: type: string description: | Instance id stamped into the snapshot metadata at capture time, rendered as the `I#` display form. total_uncompressed_bytes: type: integer format: int64 minimum: 0 description: Total uncompressed payload bytes recorded in the metadata. version: type: integer minimum: 0 description: | Snapshot wire-format version that verified (`3` for the signed envelope this gateway writes). required: - tenant_id - instance_id - total_uncompressed_bytes - version additionalProperties: false # ---- Kernel registry shapes (B6.4, feature kernel-registry-api) ------- KernelManifest: type: object description: | Signed kernel manifest. Mirrors the `tensor_wasm_jit::registry::KernelManifest` Rust struct (`#[non_exhaustive]`, so additional fields may appear in future revisions). `digest` and `signature` are fixed 32-byte arrays serialized by serde as JSON arrays of 32 integers (0-255); the HMAC `signature` tag is public by design (it authenticates authorship, the signing key is the secret). properties: name: type: string description: Stable kernel name (e.g. `matmul.f32`). version: type: string description: SemVer-style version (e.g. `1.0.0`). sm_version: type: integer minimum: 0 description: CUDA compute capability the PTX targets (e.g. 80 for sm_80). digest: type: array description: BLAKE3 hash of the PTX text (32 bytes). items: type: integer minimum: 0 maximum: 255 minItems: 32 maxItems: 32 signature: type: array description: HMAC-SHA256 tag over the canonical signed bytes (32 bytes). items: type: integer minimum: 0 maximum: 255 minItems: 32 maxItems: 32 published_unix_ms: type: integer format: int64 minimum: 0 description: Wall-clock publish timestamp (Unix millis). publisher: type: string description: Publisher identifier (typically a tenant id or signing-key id). required: - name - version - sm_version - digest - signature - published_unix_ms - publisher PublishKernelRequest: type: object description: | Body of `POST /kernels`. Mirrors the `tensor_wasm_api::kernels::PublishKernelRequest` Rust struct. properties: manifest: $ref: "#/components/schemas/KernelManifest" ptx_text: type: string description: | PTX source. The server computes BLAKE3 over the UTF-8 bytes and requires a match with `manifest.digest` before the HMAC signature check. required: [manifest, ptx_text] additionalProperties: false PublishKernelResponse: type: object description: | Body of a successful `201 Created` from `POST /kernels`. Echoes the canonical key so the client can confirm what was stored. properties: name: type: string version: type: string required: [name, version] additionalProperties: false ListKernelsResponse: type: object description: | Body of `GET /kernels`. Mirrors `tensor_wasm_api::kernels::ListKernelsResponse`. PTX text is omitted from listed manifests. properties: manifests: type: array items: $ref: "#/components/schemas/KernelManifest" offset: type: integer minimum: 0 description: Effective offset the handler used (echoes the query param). limit: type: integer minimum: 0 description: | Effective limit the handler used after server-side clamping (max 1000). required: [manifests, offset, limit] additionalProperties: false ResolveKernelResponse: type: object description: | Body of `GET /kernels/{name}/{version}`. Mirrors `tensor_wasm_api::kernels::ResolveKernelResponse`. properties: manifest: $ref: "#/components/schemas/KernelManifest" ptx_text: type: string description: PTX text whose BLAKE3 matches `manifest.digest`. required: [manifest, ptx_text] additionalProperties: false # ---- OpenAI-compatible request / response shapes (T41) --------------- CompletionsRequest: type: object description: | Body of `POST /v1/completions`. Mirrors the public OpenAI completions REST contract; every field is optional at the wire layer so SDKs that omit knobs still parse cleanly. properties: model: type: string description: | Model identifier. The gateway operator pre-aliases this to a deployed function UUID via `TENSOR_WASM_API_OPENAI_MODEL_MAP`. Unknown ids surface as `404 model_not_found`. prompt: description: | Prompt text. Accepts either a single string or an array of strings; arrays are joined with newlines before dispatch. max_tokens: type: integer minimum: 1 temperature: type: number stream: type: boolean description: | When true the response is `text/event-stream` with one `data: { ... }` SSE frame per emitted chunk and a terminal `data: [DONE]\n\n` line. echo: type: boolean n: type: integer minimum: 1 user: type: string additionalProperties: true ChatMessage: type: object description: One entry in the `messages` array of `POST /v1/chat/completions`. properties: role: type: string description: One of `system`, `user`, `assistant`, `tool`. content: description: | Message content. Either a plain string or an array of content parts (multimodal). For v0.4 only text parts are consumed; image / audio parts are silently dropped. name: type: string additionalProperties: true ChatCompletionsRequest: type: object description: | Body of `POST /v1/chat/completions`. Mirrors the public OpenAI chat-completions REST contract. properties: model: type: string messages: type: array items: $ref: "#/components/schemas/ChatMessage" max_tokens: type: integer minimum: 1 temperature: type: number stream: type: boolean n: type: integer minimum: 1 tools: description: | OpenAI tool-calling array. Accepted on the wire for forward compatibility but not yet honoured; v0.5 wires. user: type: string additionalProperties: true CompletionsChoice: type: object description: | One entry in the `choices` array of a non-streaming `/v1/completions` response. properties: text: type: string index: type: integer minimum: 0 finish_reason: type: [string, "null"] enum: [stop, length, content_filter, null] logprobs: type: "null" required: [text, index] ChatCompletionsChoice: type: object description: | One entry in the `choices` array of a non-streaming `/v1/chat/completions` response. properties: index: type: integer minimum: 0 message: type: object properties: role: type: string enum: [assistant] content: type: string required: [role, content] finish_reason: type: [string, "null"] enum: [stop, length, content_filter, null] required: [index, message] CompletionsUsage: type: object description: | Token-count block. v0.4 ships zeros across the board because the gateway does not yet wire a tokenizer; v0.5 lands a real counter (see `docs/OPENAI-COMPAT.md`). properties: prompt_tokens: type: integer minimum: 0 completion_tokens: type: integer minimum: 0 total_tokens: type: integer minimum: 0 required: [prompt_tokens, completion_tokens, total_tokens] CompletionsResponse: type: object description: | Non-streaming `/v1/completions` response. The `object` field carries the literal `"text_completion"`. properties: id: type: string object: type: string enum: [text_completion] created: type: integer format: int64 description: Unix seconds at which the response was generated. model: type: string description: Echoes the request's `model` field. choices: type: array items: $ref: "#/components/schemas/CompletionsChoice" usage: $ref: "#/components/schemas/CompletionsUsage" required: [id, object, created, model, choices, usage] ChatCompletionsResponse: type: object description: | Non-streaming `/v1/chat/completions` response. The `object` field carries the literal `"chat.completion"`. properties: id: type: string object: type: string enum: [chat.completion] created: type: integer format: int64 model: type: string choices: type: array items: $ref: "#/components/schemas/ChatCompletionsChoice" usage: $ref: "#/components/schemas/CompletionsUsage" required: [id, object, created, model, choices, usage] OpenAiErrorBody: type: object description: | Inner body of the OpenAI error envelope. The four-field shape (`message`, `type`, `param`, `code`) is what off-the-shelf OpenAI SDKs parse; this is distinct from the gateway's native `ApiErrorBody`. properties: message: type: string type: type: string description: OpenAI-conventional error category. param: type: [string, "null"] code: type: [string, "null"] required: [message, type] OpenAiError: type: object description: | Top-level OpenAI error envelope. `code: "model_not_found"` surfaces with status `404`; `code: "openai_invalid_request"` with `400`; `type: "server_error"` with `500`. properties: error: $ref: "#/components/schemas/OpenAiErrorBody" required: [error]