{
  "openapi": "3.1.0",
  "info": {
    "title": "NeuroHub API",
    "version": "1.0.0",
    "description": "NeuroHub model marketplace — OpenAI-compatible inference at /v1 and a control plane at /api/v1. Authenticate inference with an API key (Authorization: Bearer) and the control plane with either an API key or a browser session cookie.",
    "contact": { "name": "NeuroHub", "url": "https://novastep.tech" },
    "license": { "name": "Proprietary" }
  },
  "servers": [
    { "url": "https://novastep.tech", "description": "Production" },
    { "url": "/", "description": "Same origin" }
  ],
  "tags": [
    { "name": "openai", "description": "OpenAI-compatible inference surface (API key auth)." },
    { "name": "catalog", "description": "Public model catalog." },
    { "name": "orgs", "description": "Publisher organizations." },
    { "name": "auth", "description": "Accounts and sessions." },
    { "name": "billing", "description": "Wallet, usage and payouts." }
  ],
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/v1/models": {
      "get": {
        "tags": ["openai"],
        "summary": "List available chat models",
        "description": "OpenAI-compatible model list. Only chat (LLM) models that are ready to serve are returned.",
        "operationId": "openaiListModels",
        "responses": {
          "200": {
            "description": "Model list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": { "type": "string", "const": "list" },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string", "example": "llama-3.1-8b-instruct" },
                          "object": { "type": "string", "const": "model" },
                          "created": { "type": "integer" },
                          "owned_by": { "type": "string", "example": "neurohub" }
                        },
                        "required": ["id", "object"]
                      }
                    }
                  },
                  "required": ["object", "data"]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/chat/completions": {
      "post": {
        "tags": ["openai"],
        "summary": "Create a chat completion",
        "description": "OpenAI-compatible chat completions. Set \"stream\": true to receive Server-Sent Events (`data: {json}` frames terminated by `data: [DONE]`). Rate limit: 60 requests/minute.",
        "operationId": "openaiChatCompletions",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["model", "messages"],
                "properties": {
                  "model": { "type": "string", "example": "llama-3.1-8b-instruct" },
                  "messages": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["role", "content"],
                      "properties": {
                        "role": { "type": "string", "enum": ["system", "user", "assistant"] },
                        "content": { "type": "string" }
                      }
                    }
                  },
                  "temperature": { "type": "number", "minimum": 0, "maximum": 2, "default": 0.7 },
                  "max_tokens": { "type": "integer", "minimum": 1 },
                  "top_p": { "type": "number", "minimum": 0, "maximum": 1 },
                  "stop": { "type": "array", "items": { "type": "string" } },
                  "stream": { "type": "boolean", "default": false }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Completion (or an SSE stream when stream=true)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string" },
                    "object": { "type": "string", "const": "chat.completion" },
                    "model": { "type": "string" },
                    "choices": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": { "type": "integer" },
                          "finish_reason": { "type": "string", "nullable": true },
                          "message": {
                            "type": "object",
                            "properties": {
                              "role": { "type": "string", "const": "assistant" },
                              "content": { "type": "string" }
                            }
                          }
                        }
                      }
                    },
                    "usage": {
                      "type": "object",
                      "properties": {
                        "prompt_tokens": { "type": "integer" },
                        "completion_tokens": { "type": "integer" },
                        "total_tokens": { "type": "integer" }
                      }
                    }
                  }
                }
              },
              "text/event-stream": { "schema": { "type": "string" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/models": {
      "get": {
        "tags": ["catalog"],
        "summary": "Browse the catalog",
        "operationId": "catalogListModels",
        "security": [],
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Full-text / fuzzy search." },
          { "name": "type", "in": "query", "schema": { "type": "string", "enum": ["llm", "embedding", "image", "asr", "tts"] } },
          { "name": "author", "in": "query", "schema": { "type": "string" }, "description": "Author username." },
          { "name": "organization", "in": "query", "schema": { "type": "string" }, "description": "Publisher organization slug." },
          { "name": "sort", "in": "query", "schema": { "type": "string", "enum": ["popular", "newest", "price_asc", "price_desc"] } },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "page_size", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 24 } }
        ],
        "responses": {
          "200": {
            "description": "Paginated catalog",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogCard" } },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "page_size": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/models/{slug}": {
      "get": {
        "tags": ["catalog"],
        "summary": "Get a model card",
        "operationId": "catalogGetModel",
        "security": [],
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Model card", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CatalogCard" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/v1/orgs": {
      "get": {
        "tags": ["orgs"],
        "summary": "List verified publisher organizations",
        "operationId": "listOrgs",
        "security": [],
        "responses": {
          "200": {
            "description": "Organizations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/Org" } } }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": ["orgs"],
        "summary": "Create a publisher organization",
        "operationId": "createOrg",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string" },
                  "slug": { "type": "string" },
                  "description": { "type": "string" },
                  "website": { "type": "string" },
                  "contact_email": { "type": "string", "format": "email" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created (status: pending)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Org" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "409": { "description": "Slug taken" }
        }
      }
    },
    "/api/v1/orgs/{slug}": {
      "get": {
        "tags": ["orgs"],
        "summary": "Get a public organization profile",
        "description": "Verified organizations are public; pending/rejected organizations return 404 to outsiders.",
        "operationId": "getOrg",
        "security": [],
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Org" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/v1/auth/login": {
      "post": {
        "tags": ["auth"],
        "summary": "Log in and start a session",
        "operationId": "login",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "password"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string", "format": "password" },
                  "account_type": {
                    "type": "string",
                    "enum": ["user", "company"],
                    "description": "Company-area sign-in only: requires an account with account_type=company (403 otherwise)."
                  },
                  "totp_code": {
                    "type": "string",
                    "description": "6-digit TOTP code or a one-time recovery code, required when 2FA is enabled."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Session established (sets cookies and returns an access token)" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Email not confirmed, account banned, or a non-company account used the company area" }
        }
      }
    },
    "/api/v1/auth/register-company": {
      "post": {
        "tags": ["auth"],
        "summary": "Register a company account",
        "description": "Creates a user with account_type=company (is_author=true) plus a publisher organization in `pending` status owned by the new account. Staff verification is required before the organization becomes public. Rate limit: 5/hour.",
        "operationId": "registerCompany",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "password", "company_name", "legal_name"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string", "format": "password", "minLength": 8 },
                  "lang": { "type": "string", "enum": ["ru", "en"], "default": "en" },
                  "company_name": { "type": "string", "description": "Public organization name (2-128 chars)." },
                  "legal_name": { "type": "string", "description": "Official registered name, checked against documents (2-256 chars)." },
                  "website": { "type": "string", "nullable": true }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created; a confirmation email is sent", "content": { "application/json": { "schema": { "type": "object", "properties": { "user_id": { "type": "string" }, "ok": { "type": "boolean" } } } } } },
          "409": { "description": "Email already registered" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/auth/2fa": {
      "get": {
        "tags": ["auth"],
        "summary": "Two-factor status",
        "operationId": "twoFactorStatus",
        "responses": {
          "200": {
            "description": "Whether TOTP is armed and how many recovery codes remain",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": { "type": "boolean" },
                    "confirmed_at": { "type": "string", "format": "date-time", "nullable": true },
                    "recovery_codes_left": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v1/auth/2fa/setup": {
      "post": {
        "tags": ["auth"],
        "summary": "Begin TOTP enrollment",
        "description": "Returns a secret, an `otpauth://` URL and a QR SVG. 2FA stays off until `/api/v1/auth/2fa/enable` confirms a first valid code.",
        "operationId": "twoFactorSetup",
        "responses": {
          "200": {
            "description": "Pending secret + provisioning QR",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secret": { "type": "string" },
                    "otpauth_url": { "type": "string" },
                    "qr_svg": { "type": "string", "nullable": true }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v1/auth/2fa/enable": {
      "post": {
        "tags": ["auth"],
        "summary": "Confirm and arm 2FA",
        "operationId": "twoFactorEnable",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["code"],
                "properties": { "code": { "type": "string", "description": "Current 6-digit TOTP code." } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "2FA armed; one-time recovery codes returned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": { "type": "boolean" },
                    "recovery_codes": { "type": "array", "items": { "type": "string" } }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid code" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v1/auth/2fa/disable": {
      "post": {
        "tags": ["auth"],
        "summary": "Disable 2FA",
        "description": "Requires the account password and a valid second factor.",
        "operationId": "twoFactorDisable",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["password", "code"],
                "properties": {
                  "password": { "type": "string", "format": "password" },
                  "code": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "2FA disabled" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v1/auth/me": {
      "get": {
        "tags": ["auth"],
        "summary": "Current user",
        "operationId": "me",
        "responses": {
          "200": { "description": "The authenticated user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v1/wallet": {
      "get": {
        "tags": ["billing"],
        "summary": "Wallet balance",
        "operationId": "getWallet",
        "responses": {
          "200": {
            "description": "Wallet",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance_credits": { "type": "string", "example": "12.5000" },
                    "currency": { "type": "string", "const": "USD" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/v1/me/usage": {
      "get": {
        "tags": ["billing"],
        "summary": "Usage summary",
        "operationId": "getUsage",
        "responses": {
          "200": { "description": "Usage summary" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key created in the web app (API → API keys)."
      }
    },
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "username": { "type": "string", "nullable": true },
          "name": { "type": "string", "nullable": true },
          "email": { "type": "string", "nullable": true },
          "role": { "type": "string" },
          "is_author": { "type": "boolean" },
          "account_type": { "type": "string", "enum": ["user", "company"], "default": "user" },
          "lang": { "type": "string", "enum": ["ru", "en"] },
          "created_at": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "Org": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string", "nullable": true },
          "website": { "type": "string", "nullable": true },
          "logo_url": { "type": "string", "nullable": true },
          "verified": { "type": "boolean" }
        }
      },
      "CatalogCard": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "slug": { "type": "string" },
          "title": { "type": "string" },
          "type": { "type": "string" },
          "description": { "type": "string", "nullable": true },
          "author": { "type": "string", "nullable": true },
          "organization": { "nullable": true, "type": "object", "properties": { "slug": { "type": "string" }, "name": { "type": "string" } } },
          "price_per_unit_credits": { "type": "string", "nullable": true },
          "downloads": { "type": "integer" },
          "rating": { "type": "number", "nullable": true }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "message": { "type": "string" },
              "type": { "type": "string" },
              "code": { "type": "string" },
              "param": { "type": "string", "nullable": true }
            }
          }
        }
      }
    },
    "responses": {
      "Unauthorized": { "description": "Missing or invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "PaymentRequired": { "description": "Insufficient wallet balance", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "Too many requests (60/min)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  }
}
