openapi: 3.1.0 info: title: PrivateMind API version: "1.0.0" summary: OpenAI-compatible inference API for chat, embeddings, vision, tools, audio, images, and reranking. description: | The PrivateMind API is OpenAI-compatible. For chat completions and embeddings you can use the official `openai` client libraries by pointing `base_url` at `https://api.privatemind.com/v1` and using a PrivateMind API key. Endpoints that have no OpenAI equivalent (rerank, voices) are called over plain HTTP. Authentication uses a bearer token of the form `ACCESS_KEY_ID:SECRET` (the two halves joined by a colon, passed as one opaque token). Manage keys under **Settings → API Keys** in the app. See the [Authentication guide](https://docs.privatemind.com/authentication.html). Most endpoints accept a long-lived API key (`bearerAuth`). A few endpoints that act on a specific end user's conversations and sources require a **short-lived user-scoped key** (`userBearer`) minted with `POST /auth/exchange`. Each operation states which it needs. This document is the machine-readable contract for the public API surface. It is hand-maintained alongside the human docs and is the file behind the interactive [API explorer](https://docs.privatemind.com/api-explorer.html). contact: name: PrivateMind Docs url: https://docs.privatemind.com servers: - url: https://api.privatemind.com/v1 description: Production security: - bearerAuth: [] tags: - name: Chat description: Conversational text generation. - name: Embeddings description: Dense vector representations of text. - name: Rerank description: Cross-encoder relevance scoring. - name: Model catalogue description: Catalogue discovery and capabilities. - name: Audio description: Text-to-speech and transcription. - name: Images description: Text-to-image generation. - name: Usage description: Per-key usage rows and aggregated totals. - name: Conversations description: Stored conversations for a user-scoped key. - name: Sources description: Files and sources attached to a user's conversations. - name: Auth description: Minting short-lived user-scoped keys. paths: /chat/completions: post: operationId: createChatCompletion tags: [Chat] summary: Create a chat completion description: | The primary inference endpoint. Send a list of messages and receive a model-generated reply. Set `stream: true` to receive the reply as Server-Sent Events (SSE); see the [Streaming guide](https://docs.privatemind.com/streaming.html). The endpoint is a passthrough: fields not listed below (for example `tools`, `tool_choice`, `top_p`, `stop`, `seed`, `response_format`, `logit_bias`, `stream_options`) are forwarded verbatim to the upstream model, which decides whether to honour them. Whether a model honours a given field is reflected in its `supported_parameters` from `GET /models`. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ChatCompletionRequest' examples: basic: summary: Basic chat value: model: fast messages: - role: system content: You are a concise assistant. - role: user content: Explain the CAP theorem. temperature: 0.2 max_tokens: 400 reasoning: summary: Toggle thinking with reasoning_effort value: model: reasoning messages: - role: user content: Plan a four-step proof of Pythagoras. reasoning_effort: low responses: '200': description: | A chat completion. When `stream` is `false` (default) the body is a single JSON `ChatCompletionResponse`. When `stream` is `true` the body is a `text/event-stream` of `ChatCompletionChunk` objects, terminated by a `data: [DONE]` line; the final non-`[DONE]` chunk carries `usage`. content: application/json: schema: $ref: '#/components/schemas/ChatCompletionResponse' text/event-stream: schema: $ref: '#/components/schemas/ChatCompletionChunk' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /chat/count_tokens: post: operationId: countTokens tags: [Chat] summary: Count input tokens description: | Return the number of input tokens a chat request would use, without running inference. The count matches the `prompt_tokens` you would be billed for the same request to `/chat/completions`; it accounts for the system prompt, every message, tool definitions, the model's chat template, and thinking-mode tokens from `reasoning_effort`. Counting is free and writes no usage. Sampling fields (temperature, max_tokens, stream, …) are accepted but ignored. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CountTokensRequest' examples: basic: value: model: fast messages: - role: system content: You are a helpful assistant. - role: user content: Hello, how are you today? responses: '200': description: The token count and the model's context window. content: application/json: schema: $ref: '#/components/schemas/CountTokensResponse' examples: basic: value: input_tokens: 23 context_window: 131072 '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /embeddings: post: operationId: createEmbedding tags: [Embeddings] summary: Create embeddings description: | Return dense vector representations of one or more input strings. OpenAI-compatible. Only `model` is validated at the API layer; `input`, `encoding_format`, and `dimensions` are forwarded to and enforced by the embedding model. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EmbeddingRequest' examples: batch: summary: Batch of two strings value: model: kalm-embedding-gemma3-12b-2511 input: - the quick brown fox - jumps over the lazy dog responses: '200': description: A list of embedding vectors. content: application/json: schema: $ref: '#/components/schemas/EmbeddingResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /rerank: post: operationId: createRerank tags: [Rerank] summary: Rerank documents by relevance description: | Score how well each document answers a query and return them ordered by relevance, using a cross-encoder. Response shape matches Cohere's rerank API. Not part of the OpenAI SDK — call over plain HTTP. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RerankRequest' examples: capitals: value: model: qwen3-reranker-4b query: What is the capital of France? documents: - Paris is the capital of France. - London is the capital of England. - Berlin is the capital of Germany. top_n: 3 responses: '200': description: Documents scored and ordered by relevance. content: application/json: schema: $ref: '#/components/schemas/RerankResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /models: get: operationId: listModels tags: [Model catalogue] summary: List available models description: | Return the models your org is permitted to use in this environment. The list is filtered by what is live and what your org is entitled to, so it is exactly the set of model ids you can pass as `model`. Takes no query parameters and no request body. responses: '200': description: An OpenAI-shaped list of models. content: application/json: schema: $ref: '#/components/schemas/ModelList' '401': { $ref: '#/components/responses/Unauthorized' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /audio/speech: post: operationId: createSpeech tags: [Audio] summary: Synthesise speech from text description: | Generate an audio file from text. The response body is the raw audio bytes and the `Content-Type` is taken from the upstream engine (`audio/wav` if unspecified). Only `model` is validated at the API layer; `input`, `voice`, `response_format`, and `speed` are forwarded to and enforced by the speech model. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SpeechRequest' examples: hello: value: model: chatterbox-turbo voice: default input: Hello world. responses: '200': description: Raw audio bytes. The `Content-Type` is forwarded from the engine. content: audio/wav: schema: type: string format: binary audio/mpeg: schema: type: string format: binary application/octet-stream: schema: type: string format: binary '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /audio/transcriptions: post: operationId: createTranscription tags: [Audio] summary: Transcribe audio to text description: | Take an audio file and return text. Multipart form-data. The upstream ASR model is selected by the `model` query parameter (default `stt`); the multipart `model` field is forwarded to the engine but does not select the deployment. Uploads are capped at 75 MiB; larger uploads return `413` before reaching the engine. parameters: - name: model in: query required: false description: ASR deployment id or role alias to route to. Defaults to `stt`. schema: type: string maxLength: 256 default: stt requestBody: required: true content: multipart/form-data: schema: $ref: '#/components/schemas/TranscriptionRequest' responses: '200': description: | The transcription. With `response_format: json` (default) the body is a JSON object with a `text` field. `verbose_json` adds per-segment timestamps and language detection. `text`, `srt`, and `vtt` return those formats as plain text. content: application/json: schema: oneOf: - $ref: '#/components/schemas/TranscriptionResponse' - $ref: '#/components/schemas/TranscriptionVerboseJson' text/plain: schema: type: string '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '413': { $ref: '#/components/responses/PayloadTooLarge' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /voices: get: operationId: listVoices tags: [Audio] summary: List text-to-speech voices description: | Return the voice ids available for `POST /audio/speech`. The voice catalogue is read from the TTS deployment selected by the `model` query parameter (default `tts`). parameters: - name: model in: query required: false description: TTS deployment id or role alias. Defaults to `tts`. schema: type: string maxLength: 256 default: tts responses: '200': description: A list of available voices. content: application/json: schema: $ref: '#/components/schemas/VoiceList' '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /images/generations: post: operationId: createImage tags: [Images] summary: Generate an image from a prompt description: | Generate an image from a text prompt. Generation is synchronous — the request holds open until the image is ready. The image is returned inline as base64-encoded PNG under `data[].b64_json`. Set a generous client timeout. `model`, `prompt`, `size`, and `n` are validated at the API layer; `negative_prompt`, `num_inference_steps`, `guidance_scale`, `seed`, and `response_format` are forwarded to the image model, which applies their defaults. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ImageGenerationRequest' examples: redpanda: value: model: cosmos3-super-text2image prompt: a red panda on a mossy log in a misty forest at sunrise, photorealistic size: 1024x1024 responses: '200': description: The generated image(s), inline as base64. content: application/json: schema: $ref: '#/components/schemas/ImageGenerationResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /usage: get: operationId: listUsage tags: [Usage] summary: List your usage rows description: | Return flat usage rows for the authenticated API key. Results are force-scoped to the calling key (you only ever see calls that key made); rows are ordered newest first. Time bounds default to the last 30 days. `limit` defaults to 100 and is clamped to 1..1000. `offset` defaults to 0 and is capped at 100000 (beyond the cap is a `400`). `has_more` is true when the page returned exactly `limit` rows. `from` must be strictly before `to`. The filters `model`, `workspace`, `deployment`, `metadata`, `from`, and `to` narrow the result. The validator also accepts `org`, `key_id`, `user_id`, and `team_id`, but for a self-scoped key these are ignored — the caller is pinned to its own key. parameters: - { name: model, in: query, required: false, schema: { type: string }, description: Only rows for this model id. } - { name: workspace, in: query, required: false, schema: { type: string }, description: 'Traffic source filter (e.g. chat, embeddings).' } - { name: deployment, in: query, required: false, schema: { type: string }, description: Only rows for this deployment id. } - name: metadata in: query required: false schema: { type: string } description: | URL-encoded JSON object of string→string matched with containment, e.g. `{"project":"x"}`. Multiple keys AND together. Malformed JSON, a non-object, or non-string values return `400`. An empty object means no filter. - { name: from, in: query, required: false, schema: { type: string }, description: 'Lower bound (inclusive). RFC 3339 datetime or YYYY-MM-DD (UTC midnight). Default: now - 30d.' } - { name: to, in: query, required: false, schema: { type: string }, description: 'Upper bound (exclusive). RFC 3339 datetime or YYYY-MM-DD. Default: now. Must be strictly after `from`.' } - { name: limit, in: query, required: false, schema: { type: integer, format: int64, default: 100, minimum: 1, maximum: 1000 } } - { name: offset, in: query, required: false, schema: { type: integer, format: int64, default: 0, minimum: 0, maximum: 100000 } } responses: '200': description: A page of usage rows. content: application/json: schema: { $ref: '#/components/schemas/UsageListResponse' } example: success: true data: - id: 90432 org: options-it deployment: deepseek-v4-pro key_id: PMIND00000000000000000000000000 user_id: "100" model: deepseek-v4-pro prompt_tokens: 812 cached_prompt_tokens: 0 completion_tokens: 240 cost_usd: 0.0041 latency_ms: 1830 status_code: 200 created_at: "2026-06-28T12:00:00Z" workspace: chat team_id: null metadata: { project: alpha } pagination: limit: 100 offset: 0 has_more: false from: "2026-05-29T12:00:00Z" to: "2026-06-28T12:00:00Z" '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /usage/summary: get: operationId: usageSummary tags: [Usage] summary: Aggregate your usage by group description: | Server-side rollup over your usage rows: sums requests, tokens, and cost per `group_by` bucket. Same auth, scope, and filters as `/usage` plus a required `group_by`. Groups are ordered by cost descending and capped by `limit` (top-N spenders, not full enumeration). The query window may not exceed 366 days (`400` otherwise); page wider ranges by sub-window. When grouping by `metadata.`, the `null` group is untagged traffic. parameters: - name: group_by in: query required: true schema: type: string description: | One of `model`, `workspace`, `key_id`, `user_id`, `day` (UTC date of the call, rendered YYYY-MM-DD), or `metadata.` (group by a request-tag value). Unknown values return `400`. For a self-scoped key, `user_id` collapses to the caller. - { name: model, in: query, required: false, schema: { type: string } } - { name: workspace, in: query, required: false, schema: { type: string } } - { name: metadata, in: query, required: false, schema: { type: string }, description: 'Tag containment filter, same shape as on /usage.' } - { name: from, in: query, required: false, schema: { type: string }, description: 'RFC 3339 datetime or YYYY-MM-DD. Default: now - 30d.' } - { name: to, in: query, required: false, schema: { type: string }, description: 'RFC 3339 datetime or YYYY-MM-DD. Default: now. Window capped at 366 days.' } - { name: limit, in: query, required: false, schema: { type: integer, format: int64, default: 100, minimum: 1, maximum: 1000 } } - { name: offset, in: query, required: false, schema: { type: integer, format: int64, default: 0, minimum: 0, maximum: 100000 } } responses: '200': description: Per-group usage totals. content: application/json: schema: { $ref: '#/components/schemas/UsageSummaryResponse' } example: success: true group_by: model data: - group_value: deepseek-v4-pro requests: 1280 prompt_tokens: 940233 cached_prompt_tokens: 12000 completion_tokens: 410992 total_tokens: 1351225 cost_usd: 12.84 pagination: limit: 100 offset: 0 has_more: false from: "2026-05-29T12:00:00Z" to: "2026-06-28T12:00:00Z" '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/BudgetExhausted' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /auth/exchange: post: operationId: exchangeAuthToken tags: [Auth] summary: Exchange an application key for a short-lived user-scoped key description: | Mint a short-lived user-scoped key from an **application-type** API key (sent in the `Authorization` header) plus an identity-provider-issued end-user JWT (`idp_token`). The minted key is scoped to the resolved user within the application key's org and is the bearer to send on the `userBearer` endpoints (`/conversations*`, `/sources`). Only application-type keys are accepted; admin or user keys are rejected with `403`. The minted key inherits the application key's org and expires after `ttl_seconds` (clamped server-side to 60..86400, default 3600). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ExchangeRequest' examples: mint: value: idp_token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... ttl_seconds: 3600 responses: '200': description: User key minted. content: application/json: schema: $ref: '#/components/schemas/ExchangeResponseEnvelope' example: success: true data: access_key_id: PMIND00000000000000000000000000 secret: "0000000000000000000000000000000000000000000000000000000000000000" full_key: "PMIND00000000000000000000000000:0000000000000000000000000000000000000000000000000000000000000000" expires_at: "2026-06-28T13:00:00Z" user_id: "100" org_id: 1 '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '422': description: Request body failed validation (e.g. missing `idp_token`). content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } '429': { $ref: '#/components/responses/RateLimited' } '5XX': { $ref: '#/components/responses/ServerError' } /conversations: get: operationId: listConversations tags: [Conversations] summary: List conversations description: | Non-archived conversations owned by or shared with the caller, newest first. Returns conversation metadata plus a `source_count` per item; it does NOT embed message bodies. Requires a user-scoped key. security: - userBearer: [] parameters: - name: project_id in: query required: false schema: { type: string } description: Filter to a project id. Pass `none` to match conversations with no project. Omit for all. - name: cloud_agents in: query required: false schema: { type: string, enum: ["true"] } description: If `true`, returns ONLY cloud-agent conversations. Default excludes them. - name: archived in: query required: false schema: { type: string, enum: ["true"] } description: If `true`, returns ONLY archived conversations. Default returns non-archived only. responses: '200': description: Conversation list. content: application/json: schema: type: object required: [success, body, total] properties: success: { const: true } body: type: array items: { $ref: '#/components/schemas/ConversationListItem' } total: type: object required: [types] properties: types: type: array items: { type: string } example: success: true body: - id: 1207 user_id: 100 title: Quarterly planning project_id: null last_message_at: "2026-06-28T11:55:00Z" created_at: "2026-06-20T09:00:00Z" is_ephemeral: false source_count: 2 total: types: [chat] '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } '500': { $ref: '#/components/responses/ServerError' } '502': { $ref: '#/components/responses/AppBadGateway' } /conversations/{id}: get: operationId: getConversation tags: [Conversations] summary: Get one conversation description: | Full visible thread plus sources and participants. Returns `404` if the conversation is not owned by, or shared with, the caller. Requires a user-scoped key. security: - userBearer: [] parameters: - name: id in: path required: true schema: { type: string, pattern: "^[A-Za-z0-9_-]{1,64}$" } - name: before_id in: query required: false schema: { type: integer } description: Scroll up — return the next older page before this message id. - name: from_id in: query required: false schema: { type: integer } description: Reconcile anchor for the open conversation. - name: limit in: query required: false schema: { type: integer, default: 20, minimum: 1, maximum: 500 } description: Page size (default 20, capped at 500). responses: '200': description: Conversation detail. content: application/json: schema: type: object required: [success, conversation] properties: success: { const: true } conversation: { $ref: '#/components/schemas/ConversationDetail' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/AppNotFound' } '429': { $ref: '#/components/responses/RateLimited' } '502': { $ref: '#/components/responses/AppBadGateway' } /conversations/{id}/sources: get: operationId: listConversationSources tags: [Sources] summary: List sources attached to a conversation description: | Active, non-deleted source attachments for a conversation. `source_config` is redacted unless the caller owns the source. Requires a user-scoped key. security: - userBearer: [] parameters: - name: id in: path required: true schema: { type: string, pattern: "^[A-Za-z0-9_-]{1,64}$" } responses: '200': description: Source list. Here `total` is the row count. content: application/json: schema: type: object required: [success, body, total] properties: success: { const: true } body: type: array items: { $ref: '#/components/schemas/ConversationSource' } total: type: integer description: Row count. '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } '502': { $ref: '#/components/responses/AppBadGateway' } /conversations/{id}/generate: post: operationId: generateConversationReply tags: [Conversations] summary: Generate an assistant reply (SSE) description: | Stream an assistant reply for a previously-saved user message. The response is `text/event-stream`, NOT OpenAI deltas: the first frame is `data: {conversation_id, title}`, followed by agentic-event JSON frames, terminated by `data: [DONE]`. The body `conversation_id` must equal the path `id` (the path wins; a mismatch returns `400`). Accepts JSON or multipart/form-data. Requires a user-scoped key. security: - userBearer: [] parameters: - name: id in: path required: true schema: { type: string, pattern: "^[A-Za-z0-9_-]{1,64}$" } - name: model in: query required: true schema: { type: string, minLength: 1 } description: Model id to generate with. Required. - name: thinking in: query required: false schema: { type: string, enum: ["true", "false"], default: "true" } description: Set `false` to disable thinking on hybrid models. - name: webSearch in: query required: false schema: { type: string, enum: ["true", "false"], default: "false" } description: Honoured only if the org has web search enabled. requestBody: required: true content: application/json: schema: type: object required: [conversation_id, message_id] properties: conversation_id: { type: integer, minimum: 1, description: Must match the path id. } message_id: { type: integer, minimum: 1 } example: conversation_id: 1207 message_id: 88431 multipart/form-data: schema: type: object required: [conversation_id, message_id] properties: conversation_id: { type: integer, minimum: 1 } message_id: { type: integer, minimum: 1 } responses: '200': description: "SSE stream of PrivateMind-shaped frames, ending with `data: [DONE]`." content: text/event-stream: schema: type: string description: PrivateMind-shaped SSE frames. '400': { $ref: '#/components/responses/AppBadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/AppNotFound' } '429': { $ref: '#/components/responses/RateLimited' } '502': { $ref: '#/components/responses/AppBadGateway' } /conversations/delete: post: operationId: deleteConversations tags: [Conversations] summary: Batch-delete conversations description: | Permanently delete conversations the caller owns or holds owner/admin on. This is a PARTIAL delete: the allowed subset is deleted and the rest are reported in `total.skipped_ids`. A `404` is returned only when NONE of the ids are deletable. Requires a user-scoped key. security: - userBearer: [] requestBody: required: true content: application/json: schema: type: object required: [conversation_ids] properties: conversation_ids: type: array minItems: 1 items: type: [integer, string] description: Conversation ids to delete (numbers; strings are also accepted). example: conversation_ids: [1207, 1208] responses: '200': description: Delete result (possibly partial). content: application/json: schema: type: object required: [success, message, total] properties: success: { const: true } message: { type: string } total: type: object required: [deleted_ids, skipped_ids] properties: deleted_ids: type: array items: { type: integer } skipped_ids: type: array items: { type: integer } '400': { $ref: '#/components/responses/AppBadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/AppNotFound' } '429': { $ref: '#/components/responses/RateLimited' } '502': { $ref: '#/components/responses/AppBadGateway' } /conversations/{id}/files/vectorize: post: operationId: vectorizeFile tags: [Sources] summary: Ingest a file into a conversation description: | Multipart upload of one file. The server extracts text, chunks it, embeds the chunks, writes vectors plus a source row, then DISCARDS the original bytes. Pass `id=0` (or any non-existent/negative id) to have the server create a new conversation and return its id. Extracted text over 1,000,000 characters is truncated. Re-uploading the same file content (same SHA-256) by the same user skips re-embedding, links the existing source, and returns `200` with `total.duplicate=true`. Requires a user-scoped key, and the org must have file attachments enabled. Per-file cap is 50 MB. Supported extensions: txt, md, markdown, log, json, jsonl, ndjson, geojson, xml, xhtml, yaml, yml, sql, py, js, ts, r, dat, pdf, docx, pptx, odt, odp, rtf, html, htm. Legacy `.doc` is rejected; spreadsheets and images route to separate endpoints. security: - userBearer: [] parameters: - name: id in: path required: true description: Conversation id. `0`, a negative, or a non-existent id creates a new conversation. schema: type: integer - name: ephemeral in: query required: false description: When `true`, a server-created conversation is marked ephemeral (purged by the cleanup job). schema: type: boolean default: false requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary description: Single file part. The multipart field name must be exactly `file`. responses: '201': description: File vectorized and source row created. content: application/json: schema: { $ref: '#/components/schemas/VectorizeSuccessResponse' } '200': description: Duplicate (same content by the same user) — existing source linked, no re-embed. content: application/json: schema: { $ref: '#/components/schemas/VectorizeDuplicateResponse' } '400': description: No `file` part, no readable content, or a filter-passing but non-extractable type (spreadsheet, image, legacy `.doc`). content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } examples: noFile: { value: { success: false, message: No file uploaded. } } unsupported: { value: { success: false, message: Unsupported file type for extraction } } '401': { $ref: '#/components/responses/Unauthorized' } '403': description: File attachments are disabled for the org. content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } example: success: false message: file attachments enabled is not enabled for your organization code: FEATURE_DISABLED '413': description: Extractor size cap exceeded. In practice an over-50 MB file is rejected before this and surfaces as `500`. content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } '429': { $ref: '#/components/responses/RateLimited' } '500': description: Embedding count mismatch, oversize file rejected by the uploader, an unknown extension, or any unhandled error. content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } examples: generic: { value: { success: false, message: Internal server error } } mismatch: { value: { success: false, message: 'Error processing embeddings: count mismatch' } } '503': description: The embedding model service is unreachable. content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } example: success: false message: The embedding service is currently unavailable. Please try again later or contact your administrator. /sources: get: operationId: listSources tags: [Sources] summary: List all sources visible to the user description: | Return every source the user can reach: files they own, group-shared sources, and org-level sources surfaced by an admin. A source attached to multiple conversations appears once per conversation. Sources toggled inactive on their conversation are excluded. `source_config` secrets are redacted unless the caller owns the source. Requires a user-scoped key. security: - userBearer: [] responses: '200': description: Source list. content: application/json: schema: { $ref: '#/components/schemas/SourcesListResponse' } example: success: true total: 1 body: - id: 482 source_type: vectorized source_name: quarterly-report.pdf source_description: null handler: vectorFileHandler source_config: { collection_name: pmchat_1207_100 } conversation_id: 1207 is_active: true use_in_tasks: false is_owner: true created_at: "2026-06-20T09:10:00Z" updated_at: "2026-06-20T09:10:00Z" '401': { $ref: '#/components/responses/Unauthorized' } '5XX': description: Query failed. content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } example: { success: false, message: Failed to fetch sources } components: securitySchemes: bearerAuth: type: http scheme: bearer description: | Bearer token of the form `ACCESS_KEY_ID:SECRET`. The access key id is 32 characters prefixed `PMIND`; the secret is 64 hex characters. The full colon-joined string is passed as one opaque token in the `Authorization: Bearer ...` header. Used by all endpoints except the user-scoped conversation and source endpoints. userBearer: type: http scheme: bearer description: | Short-lived user-scoped key, minted with `POST /auth/exchange`, passed as `Authorization: Bearer ACCESS_KEY_ID:SECRET`. Scopes the call to a single end user. Application or admin keys are rejected with `403` on these endpoints. responses: BadRequest: description: Malformed JSON, unknown field, value out of range, or input exceeds the model context window. content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } Unauthorized: description: Authentication failed — key missing, malformed, revoked, or expired. content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } BudgetExhausted: description: Key or org spend has reached its cap. content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } Forbidden: description: The requested model is not in your org's allowed list, or the key type is not permitted on this endpoint. content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } NotFound: description: Unknown endpoint or model id. content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } PayloadTooLarge: description: Upload exceeds the 75 MiB cap (audio transcription). content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } RateLimited: description: Too many requests per minute on this key. headers: Retry-After: schema: { type: integer } description: Seconds to wait before retrying. content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } ServerError: description: Server error — timeout, memory limit, or transient failure. Retry with exponential backoff. content: application/json: schema: { $ref: '#/components/schemas/ErrorResponse' } AppBadRequest: description: Invalid request body for a conversation/source endpoint (application error envelope). content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } AppNotFound: description: Conversation, message, or model not found (application error envelope). content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } AppBadGateway: description: The application service returned a 5xx; its body is passed through (application error envelope). content: application/json: schema: { $ref: '#/components/schemas/AppErrorResponse' } schemas: # ─────────────── Chat ─────────────── ChatCompletionRequest: type: object required: [model, messages] properties: model: type: string maxLength: 256 description: Id of a model returned by `GET /models`. example: fast messages: type: array minItems: 1 items: { $ref: '#/components/schemas/ChatMessage' } temperature: type: number description: | Sampling temperature. Forwarded to the engine and not range-checked at the API layer; when omitted the model's own default applies. top_p: type: number default: 1.0 description: Nucleus sampling cutoff. max_tokens: type: integer minimum: 1 description: Maximum tokens to generate. Capped at the model's context length minus the prompt. stream: type: boolean default: false description: When true, the response is delivered as SSE chunks. stop: description: Up to four stop sequences. oneOf: - type: string - type: array maxItems: 4 items: { type: string } tools: type: array description: Function definitions the model may call. Honoured only by models whose `capabilities.tools` is true. items: { $ref: '#/components/schemas/Tool' } tool_choice: description: Controls whether/which tool the model calls. oneOf: - type: string enum: [none, auto, required] - type: object reasoning_effort: type: string enum: [off, low, medium, high] description: | Switch a model's thinking on or off. `off` disables thinking; `low`/`medium`/`high` request increasing thinking budgets. The aliases `minimal` (→ `low`) and `none` (→ `off`) are also accepted. The value is always validated, so an unrecognised value returns `400`. Effect depends on the model: models without a thinking mode silently ignore it, thinking-only models reject `off`, and non-thinking-only models reject `low`/`medium`/`high`. See the [reasoning effort docs](https://docs.privatemind.com/chat-completions.html#reasoning-effort). metadata: type: object additionalProperties: { type: string } maxProperties: 16 description: | Up to 16 string→string tags (keys ≤64 chars, values ≤512) for attributing the call. Stored on the usage row, never forwarded to the model. presence_penalty: type: number description: Forwarded to the engine; honoured per model. frequency_penalty: type: number description: Forwarded to the engine; honoured per model. seed: type: integer description: Forwarded to the engine; honoured per model. response_format: type: object description: JSON mode / structured outputs. Honoured only by models whose `capabilities.response_format` is true. additionalProperties: true ChatMessage: type: object required: [role] properties: role: type: string enum: [system, user, assistant, tool] content: description: | A string, an array of content parts for multimodal input (see [Vision](https://docs.privatemind.com/vision.html)), or `null` on assistant tool-call turns. oneOf: - type: string - type: array items: { $ref: '#/components/schemas/ContentPart' } - type: "null" name: type: string tool_calls: type: array items: { type: object } tool_call_id: type: string description: Required on `tool`-role messages — the id of the tool call being answered. ContentPart: type: object required: [type] properties: type: type: string enum: [text, image_url] text: type: string image_url: type: object properties: url: type: string description: An http(s) URL or a `data:` base64 image URI. Tool: type: object required: [type, function] properties: type: type: string enum: [function] function: type: object required: [name] properties: name: { type: string } description: { type: string } parameters: type: object description: JSON Schema for the function's arguments. ChatCompletionResponse: type: object properties: id: { type: string, example: chatcmpl-abc123 } object: { type: string, example: chat.completion } created: { type: integer, description: Unix timestamp (seconds). } model: { type: string } choices: type: array items: { $ref: '#/components/schemas/Choice' } usage: { $ref: '#/components/schemas/Usage' } Choice: type: object properties: index: { type: integer } message: { $ref: '#/components/schemas/ResponseMessage' } finish_reason: type: [string, "null"] enum: [stop, length, tool_calls, content_filter, null] description: | `stop` natural end; `length` hit max_tokens/context; `tool_calls` model wants to call a tool; `content_filter` output blocked. ResponseMessage: type: object properties: role: { type: string, example: assistant } content: type: [string, "null"] reasoning: type: [string, "null"] description: Chain-of-thought, present only for reasoning models that emit it. See Streaming → Reasoning output. tool_calls: type: array items: { type: object } ChatCompletionChunk: type: object description: | One SSE chunk emitted when `stream` is true. Chunks carry `choices[].delta` with incremental content. The stream ends with a literal `data: [DONE]` line. The final non-`[DONE]` chunk carries `usage`. A mid-stream error arrives as a chunk with an `error` field. properties: id: { type: string } object: { type: string, example: chat.completion.chunk } created: { type: integer } model: { type: string } choices: type: array items: type: object properties: index: { type: integer } delta: type: object properties: role: { type: string } content: type: [string, "null"] reasoning: type: [string, "null"] tool_calls: type: array items: { type: object } finish_reason: type: [string, "null"] usage: oneOf: - $ref: '#/components/schemas/Usage' - type: "null" description: Present only on the final non-`[DONE]` chunk. error: $ref: '#/components/schemas/ErrorObject' Usage: type: object properties: prompt_tokens: type: integer description: Input tokens. Billed against your key. completion_tokens: type: integer description: Output tokens. Billed against your key. Always 0 for rerank. total_tokens: type: integer # ─────────────── Embeddings ─────────────── EmbeddingRequest: type: object required: [model, input] additionalProperties: true properties: model: type: string maxLength: 256 description: Embedding model id (max 256 bytes). Only this field is validated at the API layer. example: kalm-embedding-gemma3-12b-2511 input: description: A single string or an array of strings. Forwarded to and enforced by the model. oneOf: - type: string - type: array items: { type: string } encoding_format: type: string enum: [float, base64] default: float description: base64 is ~4× smaller on the wire. Enforced by the model, not the API layer. dimensions: type: integer minimum: 1 description: Truncate the output vector to N dimensions where the model supports it. EmbeddingResponse: type: object required: [object, data, model, usage] properties: object: { type: string, example: list } data: type: array items: { $ref: '#/components/schemas/EmbeddingObject' } model: { type: string } usage: { $ref: '#/components/schemas/EmbeddingUsage' } EmbeddingObject: type: object properties: object: { type: string, example: embedding } index: { type: integer } embedding: description: A float vector, or a base64-encoded float32 string when `encoding_format` is `base64`. oneOf: - type: array items: { type: number } - type: string EmbeddingUsage: type: object required: [prompt_tokens, total_tokens] properties: prompt_tokens: type: integer description: Input tokens. Billed against your key. total_tokens: type: integer # ─────────────── Rerank ─────────────── RerankRequest: type: object required: [model, query, documents] properties: model: type: string example: qwen3-reranker-4b query: type: string description: The search query to score documents against. documents: type: array items: { type: string } description: Strings to rank. Each is read together with the query. top_n: type: integer minimum: 1 description: Return only the N highest-scoring documents. Defaults to all. RerankResponse: type: object properties: id: { type: string } model: { type: string } results: type: array description: Sorted by `relevance_score` descending. items: { $ref: '#/components/schemas/RerankResult' } usage: { $ref: '#/components/schemas/Usage' } RerankResult: type: object properties: index: type: integer description: Position of this document in the request `documents` array. relevance_score: type: number description: Relevance, 0 to 1. document: type: object properties: text: { type: string } # ─────────────── Models ─────────────── ModelList: type: object properties: object: { type: string, example: list } data: type: array items: { $ref: '#/components/schemas/Model' } Model: type: object required: [id, object, created, owned_by, supported_parameters, aliases, capabilities] description: | OpenAI-shaped model object. Optional fields are omitted entirely (not null) when unset, so clients should treat an absent field as unset. properties: id: { type: string, example: deepseek-v4-pro } object: { type: string, example: model } created: type: integer format: int64 description: Unix timestamp (seconds), set per response. Always present. owned_by: type: string description: Owning org slug. example: options-it context_length: type: integer description: Maximum total token window (prompt + completion). Omitted when no limit is declared. model_type: type: string description: | Free-form model class (not a validated enum). Known values: `chat`, `vision-chat`, `embeddings`, `reranker`, `ocr`, `ocr-vision`, `tts`, `asr`, `image-gen`. Omitted when not provided. example: chat model_full_name: type: string description: Human-friendly display name. Falls back to `id` when absent. model_icon_url: type: string description: Relative path to a PrivateMind-hosted brand icon. Absent when not set. capabilities: { $ref: '#/components/schemas/Capabilities' } supported_parameters: type: array items: { type: string } description: OpenRouter-style list of accepted parameters. Empty for non-chat models. aliases: type: array items: { type: string } description: Role aliases that also resolve to this model (e.g. `reasoning`, `fast`). Always present, may be empty. example: [reasoning] cost: { $ref: '#/components/schemas/Cost' } description: type: string description: Short human description of the model, when configured. use_when: type: string description: Guidance on when to choose this model (for model pickers), when configured. Capabilities: type: object description: | Boolean capability flags. Only capabilities the model has are present (an absent flag means false), so an all-false model serialises as `{}`. properties: tools: { type: boolean } response_format: { type: boolean } reasoning_effort: { type: boolean } image_input: { type: boolean } Cost: type: object description: | Published list price for the model. Rates are USD per 1,000,000 tokens (per 1M). Present on catalogued models; omitted when no price is set. properties: input_per_m_token: type: number description: USD per 1M prompt (input) tokens. output_per_m_token: type: number description: USD per 1M completion (output) tokens. image_per_generation: type: number description: USD per generated image. Present only on image-generation models. CountTokensRequest: type: object required: [model, messages] additionalProperties: true properties: model: type: string maxLength: 256 description: Chat model id or alias, resolved exactly as `/chat/completions` resolves it. example: fast messages: type: array minItems: 1 items: { $ref: '#/components/schemas/ChatMessage' } tools: type: array items: { $ref: '#/components/schemas/Tool' } description: Counted for models whose chat template renders tools into the prompt. reasoning_effort: type: string enum: [off, low, medium, high] description: Pass the same value you will send to the completion; on thinking models it changes the count. CountTokensResponse: type: object properties: input_tokens: type: integer description: Input tokens the request would consume — the same number billed as `prompt_tokens` on the matching completion. context_window: type: integer description: The model's maximum context length in tokens. # ─────────────── Audio ─────────────── SpeechRequest: type: object required: [model] additionalProperties: true properties: model: type: string maxLength: 256 description: TTS deployment id or role alias. Only this field is validated at the API layer. example: chatterbox-turbo voice: type: string description: Voice id (list with `GET /voices`). Required by the engine. example: default input: type: string description: Text to synthesise. Required by the engine. response_format: type: string enum: [mp3, opus, aac, flac, wav, pcm] default: mp3 description: Engine support varies. The response `Content-Type` is taken from the engine regardless. speed: type: number default: 1.0 description: Playback speed multiplier. TranscriptionRequest: type: object required: [file] additionalProperties: true properties: file: type: string format: binary description: 'Audio file. Supported: mp3, mp4, m4a, wav, webm, flac, ogg, mpga, mpeg. Max 75 MiB.' model: type: string description: Forwarded to the engine. Does NOT select the deployment — use the `model` query parameter for that. language: type: string description: ISO-639-1 code (en, fr, de, ...). Improves accuracy and latency when known. prompt: type: string description: Priming text — names, jargon, or context. response_format: type: string enum: [json, text, srt, verbose_json, vtt] default: json TranscriptionResponse: type: object required: [text] properties: text: type: string example: Hello, this is a test transcription. TranscriptionVerboseJson: type: object description: The `verbose_json` response. Adds language detection, duration, and per-segment timestamps. properties: text: { type: string } language: type: [string, "null"] duration: type: [number, "null"] segments: type: [array, "null"] items: type: object properties: id: { type: integer } start: { type: number } end: { type: number } text: { type: string } VoiceList: description: Engine-defined voice catalogue, forwarded verbatim. Usually an array of voice id strings. oneOf: - type: array items: { type: string } - type: object properties: data: type: array items: type: object properties: id: { type: string } # ─────────────── Images ─────────────── ImageGenerationRequest: type: object required: [model, prompt, size] additionalProperties: true properties: model: type: string maxLength: 256 description: Image model id (type `image-gen`). The alias `t2i` also resolves to this model. example: cosmos3-super-text2image prompt: type: string description: Text description of the image to generate. size: type: string pattern: '^[1-9]\d*x[1-9]\d*$' description: Required. Output resolution as WIDTHxHEIGHT. A missing or malformed value is rejected with a 400. example: 1024x1024 n: type: integer default: 1 minimum: 1 maximum: 10 description: | Number of images. A value outside 1..10 is rejected with a 400; each image counts as one rate-limit unit. negative_prompt: type: string description: Text describing what to avoid in the image. num_inference_steps: type: integer default: 50 description: Diffusion steps. Fewer is faster; more can refine detail. Default applied by the model. guidance_scale: type: number default: 4.0 description: How strongly the image adheres to the prompt. Default applied by the model. seed: type: integer description: Fix for reproducible output. Omit for a random image each call. response_format: type: string enum: [b64_json] default: b64_json description: Image is returned inline as base64. `url` is not supported. ImageGenerationResponse: type: object properties: created: type: integer description: Unix timestamp (seconds). data: type: array items: type: object properties: b64_json: type: string description: Base64-encoded PNG. # ─────────────── Usage ─────────────── UsageRow: type: object description: A single usage row. required: - id - org - deployment - key_id - user_id - model - prompt_tokens - cached_prompt_tokens - completion_tokens - cost_usd - latency_ms - status_code - created_at - workspace - team_id - metadata properties: id: { type: integer, format: int64 } org: { type: string } deployment: { type: string } key_id: { type: string } user_id: { type: string } model: { type: string } prompt_tokens: { type: integer, format: int32 } cached_prompt_tokens: type: integer format: int32 description: Subset of prompt_tokens served from the prefix cache. 0 unless enabled at the model. completion_tokens: { type: integer, format: int32 } cost_usd: { type: number, format: double } latency_ms: { type: integer, format: int32 } status_code: { type: integer, format: int32 } created_at: { type: string, format: date-time } workspace: { type: string } team_id: type: [integer, "null"] format: int32 metadata: type: [object, "null"] additionalProperties: { type: string } description: Caller-supplied request tags. null for untagged requests. UsagePagination: type: object required: [limit, offset, has_more, from, to] properties: limit: { type: integer, format: int64 } offset: { type: integer, format: int64 } has_more: { type: boolean } from: { type: string, format: date-time } to: { type: string, format: date-time } UsageListResponse: type: object required: [success, data, pagination] properties: success: { type: boolean, enum: [true] } data: type: array items: { $ref: '#/components/schemas/UsageRow' } pagination: { $ref: '#/components/schemas/UsagePagination' } UsageSummaryRow: type: object required: - group_value - requests - prompt_tokens - cached_prompt_tokens - completion_tokens - total_tokens - cost_usd properties: group_value: type: [string, "null"] description: Value of the grouping dimension. null is the untagged bucket for metadata.. requests: { type: integer, format: int64 } prompt_tokens: { type: integer, format: int64 } cached_prompt_tokens: { type: integer, format: int64 } completion_tokens: { type: integer, format: int64 } total_tokens: type: integer format: int64 description: prompt_tokens + completion_tokens. cost_usd: type: number format: double description: Rounded sum, suitable for reporting; not exact invoice reconciliation. UsageSummaryResponse: type: object required: [success, group_by, data, pagination] properties: success: { type: boolean, enum: [true] } group_by: type: string description: Echo of the requested group_by. data: type: array items: { $ref: '#/components/schemas/UsageSummaryRow' } pagination: { $ref: '#/components/schemas/UsagePagination' } # ─────────────── Auth ─────────────── ExchangeRequest: type: object required: [idp_token] properties: idp_token: type: string description: Identity-provider-issued end-user JWT. Verified server-side; yields the user's identity. ttl_seconds: type: [integer, "null"] format: int64 default: 3600 description: | Requested key lifetime in seconds. Clamped server-side to 60..86400 (out-of-range values are clamped, not rejected). Omitted uses the default (3600). ExchangeResponseEnvelope: type: object required: [success, data] properties: success: { const: true } data: { $ref: '#/components/schemas/ExchangeResponse' } ExchangeResponse: type: object required: [access_key_id, secret, full_key, expires_at, user_id, org_id] properties: access_key_id: type: string description: Public id of the minted user key. secret: type: string description: Secret half of the minted user key. full_key: type: string description: Convenience concatenation `access_key_id:secret` — the bearer to send on user-scoped endpoints. expires_at: type: string format: date-time description: When the minted key expires (now + clamped ttl). user_id: type: string description: Resolved user id (stringified). org_id: type: integer format: int32 description: Org the user key is scoped to (inherited from the application key). # ─────────────── Conversations & Sources ─────────────── ConversationListItem: type: object additionalProperties: true description: One conversation row plus `source_count`. No `messages` field — list rows do not embed message bodies. required: [id, user_id, last_message_at, source_count] properties: id: { type: integer } user_id: { type: integer } title: { type: [string, "null"] } project_id: { type: [integer, "null"] } cloud_agent_task_id: { type: [integer, "null"] } archived_at: { type: [string, "null"], format: date-time } last_message_at: { type: string, format: date-time } created_at: { type: string, format: date-time } is_ephemeral: { type: boolean } source_count: { type: integer } type: { type: [string, "null"], description: Optional; feeds the total.types set. } EmbeddedSource: type: object description: A source as embedded in `GET /conversations/{id}`. required: [id, source_type, source_name, is_owner] properties: id: { type: integer } source_type: { type: string } source_name: { type: string } source_config: type: object additionalProperties: true description: Redacted unless the caller owns the source. is_active: { type: [boolean, "null"] } use_in_tasks: { type: [boolean, "null"] } handler: { type: [string, "null"] } source_description: { type: [string, "null"] } is_owner: { type: boolean } ConversationSource: type: object description: A source row returned by `GET /conversations/{id}/sources` (richer than the embedded form). required: [id, source_type, source_name, conversation_id, is_owner] properties: id: { type: integer } source_type: { type: string } source_name: { type: string } source_config: type: object additionalProperties: true description: Redacted unless the caller owns the source. handler: { type: [string, "null"] } source_description: { type: [string, "null"] } created_at: { type: string, format: date-time } updated_at: { type: [string, "null"], format: date-time } metadata: { type: [object, "null"], additionalProperties: true } host_identifier: { type: [string, "null"] } conversation_id: { type: integer } is_active: { type: [boolean, "null"] } use_in_tasks: { type: [boolean, "null"] } source_origin: { type: [string, "null"] } is_owner: { type: boolean } Participant: type: object required: [user_id, role] properties: user_id: { type: integer } role: { type: string, description: owner | admin | member | viewer } first_name: { type: [string, "null"] } last_name: { type: [string, "null"] } email: { type: [string, "null"] } joined_at: { type: [string, "null"], format: date-time } Message: type: object additionalProperties: true required: [id, conversation_id, message, role, created_at] properties: id: { type: integer } conversation_id: { type: integer } user_id: { type: [integer, "null"] } message: { type: string, description: Decrypted server-side. } reasoning_content: { type: [string, "null"] } role: { type: string, description: user | assistant | tool | system } created_at: { type: string, format: date-time } is_read: { type: [boolean, "null"] } attachments: type: [array, "null"] items: { type: object, additionalProperties: true } model_name: { type: [string, "null"] } tool_call_id: { type: [string, "null"] } tool_name: { type: [string, "null"] } tool_status: { type: [string, "null"], description: ok | error | running } finish_reason: { type: [string, "null"], description: tool_calls | stop | length } sender_display_name: { type: [string, "null"], description: Resolved author name on user rows. } turn_id: { type: [string, "null"] } parent_message_id: { type: [integer, "null"] } active_sibling_message: { type: boolean } sibling_ids: type: array items: { type: integer } metadata: { type: object, additionalProperties: true } decryption_failed: { type: boolean, description: Present (true) only when the row could not be decrypted. } ConversationDetail: type: object additionalProperties: true description: The conversation row plus sources, participants, messages, and a pagination flag. required: [id, user_id, sources, participants, messages] properties: id: { type: integer } user_id: { type: integer } title: { type: [string, "null"] } project_id: { type: [integer, "null"] } cloud_agent_task_id: { type: [integer, "null"] } archived_at: { type: [string, "null"], format: date-time } last_message_at: { type: string, format: date-time } created_at: { type: string, format: date-time } has_more: { type: boolean, description: Older-page pagination flag. } sources: type: array items: { $ref: '#/components/schemas/EmbeddedSource' } participants: type: array items: { $ref: '#/components/schemas/Participant' } messages: type: array items: { $ref: '#/components/schemas/Message' } SourcesListResponse: type: object required: [success, body, total] properties: success: { type: boolean } total: type: integer description: Count of rows in body. body: type: array items: { $ref: '#/components/schemas/SourceRow' } SourceRow: type: object description: | A source visible to the user. conversation_id/is_active/use_in_tasks are null for org-level sources not bound to a conversation. required: [id, source_type, source_name, source_config, handler, created_at, updated_at] properties: id: { type: integer } source_type: type: string description: e.g. vectorized, tabular, custom, mcp_web. source_name: { type: string } source_description: { type: [string, "null"] } handler: type: [string, "null"] description: Retrieval-time handler, e.g. vectorFileHandler. source_config: type: object additionalProperties: true description: Handler-specific config; secrets redacted unless caller owns the source. metadata: { type: [object, "null"], additionalProperties: true } host_identifier: { type: [string, "null"] } conversation_id: { type: [integer, "null"] } is_active: { type: [boolean, "null"] } use_in_tasks: { type: [boolean, "null"] } source_origin: { type: [string, "null"] } is_owner: { type: [boolean, "null"] } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } VectorizeSuccessResponse: type: object required: [success, message, total] properties: success: { type: boolean } message: type: string description: | e.g. "File vectorized with N chunks successfully", or for truncated input "File partially vectorized (N chunks from first 1000000 of M characters)". total: type: object required: [conversation_id, vectorized_file] properties: conversation_id: { type: integer } vectorized_file: type: object required: [id, file_name, collection_name, file_type, file_size, created_at] properties: id: { type: integer } file_name: { type: string } collection_name: type: string description: Format pmchat__. file_type: type: string description: Client-supplied MIME type. file_size: { type: integer } created_at: { type: string, format: date-time } VectorizeDuplicateResponse: type: object required: [success, message, total] properties: success: { type: boolean } message: type: string example: '"quarterly-report.pdf" already uploaded — linked existing source' total: type: object required: [duplicate, conversation_id, vectorized_file] properties: duplicate: { type: boolean, enum: [true] } conversation_id: { type: integer } vectorized_file: type: object required: [id, file_name, collection_name] properties: id: { type: integer } file_name: { type: string } collection_name: { type: [string, "null"] } # ─────────────── Errors ─────────────── ErrorResponse: type: object description: Error envelope. Matches the OpenAI shape. properties: error: { $ref: '#/components/schemas/ErrorObject' } ErrorObject: type: object properties: message: type: string description: Human-readable description. May change between releases — do not branch on it. type: type: string description: Error category. code: type: [string, "null"] description: | Stable machine-readable code. Gate retry/branching logic on this. Known values include `budget_exceeded`, `rpm_exceeded`, `timeout`, `context_exceeded`. AppErrorResponse: type: object description: | Error envelope used by the conversation and source endpoints that are not OpenAI-shaped. Distinct from `ErrorResponse`. required: [success, message] properties: success: type: boolean const: false message: type: string code: type: string description: Present on some errors (e.g. MODEL_NOT_FOUND, FEATURE_DISABLED). total: type: object additionalProperties: true description: Present on the batch-delete 404 (carries deleted_ids and skipped_ids).