{
  "openapi": "3.1.0",
  "info": {
    "title": "ZeroLog API",
    "version": "1.0.0",
    "summary": "OpenAI-compatible model access, your client token, and inbound webhooks.",
    "description": "The public ZeroLog API. It covers what a customer with an own account is meant to call from their own code: an OpenAI-compatible chat completions endpoint and model list, the endpoint that hands a signed-in user their client token, and two inbound webhooks that a user creates in the app.\n\nEverything else under `/api/` is the private interface of the ZeroLog apps and their clients. It can change without notice and is not covered here.\n\nAll examples use invented data. Replace `mhc-1042-REPLACE_WITH_YOUR_TOKEN` with your own token.",
    "contact": {
      "name": "ZeroLog",
      "email": "info@zerolog.ch",
      "url": "https://zerolog.ch/docs/api"
    },
    "termsOfService": "https://zerolog.ch/legal"
  },
  "servers": [
    {
      "url": "https://zerolog.ch",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Guides: quickstart, authentication, errors, limits",
    "url": "https://zerolog.ch/docs/api"
  },
  "tags": [
    {
      "name": "Models",
      "description": "OpenAI-compatible inference. Billed to your ZeroLog balance, or to your own provider key for `byok/…` models."
    },
    {
      "name": "Authentication",
      "description": "Getting the client token for the endpoints above."
    },
    {
      "name": "Webhooks",
      "description": "Inbound webhooks. You create them in the app (Automation and container settings); the token in the URL is the only credential and is shown once."
    }
  ],
  "paths": {
    "/api/cli/v1/models": {
      "get": {
        "operationId": "listModels",
        "tags": [
          "Models"
        ],
        "summary": "List models",
        "description": "OpenAI-shaped model list for clients that probe `/models`. It returns the model your account resolves to by default, if your account may use it. The list can be empty; you can still send any model id from the catalogue that your account may use. It is not the full catalogue.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Model list.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlPrivate"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelList"
                },
                "examples": {
                  "default": {
                    "value": {
                      "object": "list",
                      "data": [
                        {
                          "id": "featherless/google/gemma-4-31b-it",
                          "object": "model",
                          "created": 0,
                          "owned_by": "zerolog"
                        }
                      ]
                    }
                  },
                  "empty": {
                    "summary": "No usable default model",
                    "value": {
                      "object": "list",
                      "data": []
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidToken"
          }
        }
      }
    },
    "/api/cli/v1/chat/completions": {
      "post": {
        "operationId": "createChatCompletion",
        "tags": [
          "Models"
        ],
        "summary": "Create a chat completion",
        "description": "OpenAI-compatible chat completions. The request body is passed to the model provider in OpenAI form, so the usual fields (`messages`, `tools`, `tool_choice`, `temperature`, `max_tokens`, …) work. Only `model`, `messages` and `stream` are interpreted by ZeroLog itself.\n\n**Model.** Without `model`, the operator's catalogue default is used. A model your account may not use is refused with `403 model_not_available` before any provider is called. `byok/<provider>/<model>` runs on your own provider key and never falls back to the shared pool.\n\n**Fallback.** If the chosen model fails upstream, ZeroLog may answer with another model. It then sets the `X-ZeroLog-Served-Model`, `X-ZeroLog-Rerouted-From` and `X-ZeroLog-Model-Notice` headers and puts a one-line `[ZeroLog] … is unavailable` notice at the start of the answer. Send `X-ZeroLog-Reroute: off` to get the error instead.\n\n**Streaming.** With `\"stream\": true` the answer is `text/event-stream` in OpenAI chunk format, ending with `data: [DONE]`. ZeroLog asks the provider for a final usage chunk.\n\n**Billing.** Each answer is billed to your balance (or your own key for `byok/…`). A stream that the client aborts is still billed for what was produced.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "name": "X-ZeroLog-Reroute",
            "in": "header",
            "required": false,
            "description": "`off` disables the automatic fallback to another model. You then get the provider's error instead of a different model.",
            "schema": {
              "type": "string",
              "enum": [
                "off"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "OpenAI chat completions request. Maximum body size 32 MB.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatCompletionRequest"
              },
              "examples": {
                "simple": {
                  "summary": "Single answer",
                  "value": {
                    "model": "featherless/google/gemma-4-31b-it",
                    "messages": [
                      {
                        "role": "user",
                        "content": "Write a haiku about Swiss trains."
                      }
                    ]
                  }
                },
                "stream": {
                  "summary": "Streamed answer",
                  "value": {
                    "model": "featherless/google/gemma-4-31b-it",
                    "stream": true,
                    "messages": [
                      {
                        "role": "system",
                        "content": "You are terse."
                      },
                      {
                        "role": "user",
                        "content": "Name three Swiss lakes."
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The completion. JSON without `stream`, an event stream with `\"stream\": true`.",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlPrivate"
              },
              "X-ZeroLog-Served-Model": {
                "$ref": "#/components/headers/ServedModel"
              },
              "X-ZeroLog-Served-Provider": {
                "$ref": "#/components/headers/ServedProvider"
              },
              "X-ZeroLog-Rerouted-From": {
                "$ref": "#/components/headers/ReroutedFrom"
              },
              "X-ZeroLog-Model-Notice": {
                "$ref": "#/components/headers/ModelNotice"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatCompletion"
                },
                "examples": {
                  "default": {
                    "value": {
                      "id": "chatcmpl-example-1",
                      "object": "chat.completion",
                      "created": 1793700000,
                      "model": "featherless/google/gemma-4-31b-it",
                      "choices": [
                        {
                          "index": 0,
                          "message": {
                            "role": "assistant",
                            "content": "Rails through the pass\nsnow on the windows, warm tea\nthe clock is on time"
                          },
                          "finish_reason": "stop"
                        }
                      ],
                      "usage": {
                        "prompt_tokens": 16,
                        "completion_tokens": 19,
                        "total_tokens": 35
                      }
                    }
                  }
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Server-sent events. Each `data:` line carries one `ChatCompletionChunk` as JSON; the stream ends with `data: [DONE]`."
                },
                "examples": {
                  "default": {
                    "value": "data: {\"id\":\"chatcmpl-example-2\",\"object\":\"chat.completion.chunk\",\"model\":\"featherless/google/gemma-4-31b-it\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Lake Geneva\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-example-2\",\"object\":\"chat.completion.chunk\",\"choices\":[],\"usage\":{\"prompt_tokens\":21,\"completion_tokens\":9,\"total_tokens\":30}}\n\ndata: [DONE]\n\n"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidToken"
          },
          "402": {
            "$ref": "#/components/responses/NoCredit"
          },
          "403": {
            "$ref": "#/components/responses/ModelForbidden"
          },
          "404": {
            "$ref": "#/components/responses/UpstreamClientError"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/UpstreamRateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          },
          "4XX": {
            "$ref": "#/components/responses/UpstreamClientError"
          }
        }
      }
    },
    "/api/cli/token": {
      "get": {
        "operationId": "getClientToken",
        "tags": [
          "Authentication"
        ],
        "summary": "Get your client token",
        "description": "Returns the client token of the signed-in account and the base URL to use it with. Needs a browser session on zerolog.ch: open the address while signed in, or let ZeroLog Code fetch it with `zerolog login`.\n\nThe token is derived from your account. It does not expire. It stops working when the account is disabled. It cannot be revoked on its own from the app today; if it leaks, write to info@zerolog.ch.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your token and base URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientToken"
                },
                "examples": {
                  "default": {
                    "value": {
                      "token": "mhc-1042-REPLACE_WITH_YOUR_TOKEN",
                      "base_url": "https://zerolog.ch/api/cli/v1",
                      "note": "Bearer token for the zerocode terminal client. Pick the model in zerocode; it is sent as body.model."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the account is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SimpleError"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The session may not do this: for example a session derived from a device token, or a paused account. `code` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodedError"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "pairing needs a browser sign-in, not a session derived from a device token",
                      "code": "needs_web_session"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/hook/{token}": {
      "post": {
        "operationId": "triggerGoalWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Trigger a goal webhook",
        "description": "Starts the goal run that the webhook was created for, in its chat and container. Created in the app; the token (`mhw_…`) is shown once and stored only as a hash.\n\nThe call returns `202` as soon as the run is queued. `status_url` points to the job in the app and needs your browser session; it is not a public endpoint.\n\nEvery method other than POST answers `405`, so that opening the link in a browser never starts a paid run. Maximum body size 32 kB.\n\n**Limits** are set per webhook: 1–60 triggers per minute (default 6) and 1–1000 per day (default 100). The account needs the goal mode permission and credit.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/GoalHookToken"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GoalHookTrigger"
              },
              "examples": {
                "empty": {
                  "summary": "Run the stored objective",
                  "value": {}
                },
                "withPrompt": {
                  "summary": "Append trigger text (webhook in append mode)",
                  "value": {
                    "prompt": "Deploy finished for commit 3f2a91c. Check the release notes."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Run queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GoalHookAccepted"
                },
                "examples": {
                  "default": {
                    "value": {
                      "job_id": 5821,
                      "status_url": "/api/jobs/5821"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/WebhookBadRequest"
          },
          "402": {
            "$ref": "#/components/responses/WebhookNoFunds"
          },
          "403": {
            "$ref": "#/components/responses/WebhookForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WebhookUnknown"
          },
          "405": {
            "$ref": "#/components/responses/WebhookMethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/WebhookConflict"
          },
          "413": {
            "$ref": "#/components/responses/WebhookTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/WebhookRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/WebhookInternal"
          }
        }
      }
    },
    "/api/inject/{token}": {
      "post": {
        "operationId": "injectWebhookEvent",
        "tags": [
          "Webhooks"
        ],
        "summary": "Deliver an event into a container",
        "description": "Appends the request as one JSON line to `/work/.webhooks/incoming.jsonl` in the container the webhook belongs to. Point GitHub, Stripe, Zapier or your own service at this URL; an agent in the container can then read the events. The token (`mhin_…`) is shown once when you create the webhook.\n\nStored per event: time, method, request headers and the parsed JSON body. `Authorization`, `Cookie`, `X-Api-Key`, `X-Auth-Token` and `Proxy-Authorization` are replaced by `‹redacted›` before storage.\n\n`202` is returned for every accepted event. `ok: false` with `status: \"error\"` means the event was counted but could not be written, usually because the container is not running.\n\nEvery method other than POST answers `405`. Maximum body size 256 kB. **Limit** per webhook: 1–120 events per minute (default 60).",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/InjectToken"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Any JSON object. Stored as sent.",
                "additionalProperties": true
              },
              "examples": {
                "github": {
                  "summary": "A push event, shortened",
                  "value": {
                    "ref": "refs/heads/main",
                    "after": "3f2a91c",
                    "repository": {
                      "full_name": "example-org/example-repo"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Event accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InjectAccepted"
                },
                "examples": {
                  "stored": {
                    "value": {
                      "ok": true,
                      "status": "stored",
                      "path": "/work/.webhooks/incoming.jsonl"
                    }
                  },
                  "notWritten": {
                    "summary": "Container not running",
                    "value": {
                      "ok": false,
                      "status": "error",
                      "path": "/work/.webhooks/incoming.jsonl"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/WebhookBadRequest"
          },
          "404": {
            "$ref": "#/components/responses/WebhookUnknown"
          },
          "405": {
            "$ref": "#/components/responses/InjectMethodNotAllowed"
          },
          "413": {
            "$ref": "#/components/responses/WebhookTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/WebhookRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/WebhookInternal"
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "BadRequest": {
        "description": "No model given and no default configured, the body is not valid JSON (then `{\"error\": \"…\"}` as a plain string), or the provider rejected the request (`upstream_status` is then set).",
        "headers": {
          "X-ZeroLog-Upstream-Status": {
            "$ref": "#/components/headers/UpstreamStatus"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorOrSimple"
            },
            "examples": {
              "noModel": {
                "value": {
                  "error": {
                    "message": "no model configured",
                    "type": "invalid_request_error"
                  }
                }
              }
            }
          }
        }
      },
      "InjectMethodNotAllowed": {
        "description": "Any method other than POST.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "use POST to inject an event"
                }
              }
            }
          }
        }
      },
      "InvalidToken": {
        "description": "Missing, malformed or wrong client token, or the account is disabled. A request without a token in the expected form and with a body over 64 kB is refused before it is read, with `{\"error\": \"unauthorized\"}`.",
        "content": {
          "application/json": {
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/AuthError"
                },
                {
                  "$ref": "#/components/schemas/SimpleError"
                }
              ]
            },
            "examples": {
              "default": {
                "value": {
                  "type": "error",
                  "error": {
                    "type": "authentication_error",
                    "message": "invalid token"
                  }
                }
              }
            }
          }
        }
      },
      "ModelForbidden": {
        "description": "Your account may not use this model (`model_not_available`), or no usable key is available for it (for `byok/…`: your own key is missing or invalid).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": {
                    "message": "Dieses Modell steht auf deinem Konto nicht zur Verfuegung. Waehle ein Modell aus dem Katalog (GET /api/models).",
                    "type": "permission_error",
                    "code": "model_not_available"
                  }
                }
              }
            }
          }
        }
      },
      "NoCredit": {
        "description": "Your balance is used up, or the shared pool is empty.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": {
                    "message": "no credit left — top up in Billing",
                    "type": "billing_error"
                  }
                }
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body is larger than this endpoint accepts, or the provider refused it as too large (`upstream_status` is then set).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorOrSimple"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "request entity too large"
                }
              }
            }
          }
        }
      },
      "Unavailable": {
        "description": "No provider could answer (provider down, timed out or refused our key), or customer inference on this provider is paused (`provider_temporarily_paused`), or the provider behind the model has no working key on our side (`provider_unavailable`). 401 and 403 from a provider are reported as 503, because they concern ZeroLog's key and not yours.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-ZeroLog-Upstream-Status": {
            "$ref": "#/components/headers/UpstreamStatus"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "paused": {
                "value": {
                  "error": {
                    "message": "Customer inference through Featherless is temporarily paused while we complete the provider data protection agreements. Your account, credit, and conversation history remain unchanged.",
                    "type": "api_error",
                    "code": "provider_temporarily_paused"
                  }
                }
              },
              "upstream": {
                "value": {
                  "error": {
                    "message": "upstream unreachable",
                    "type": "api_error",
                    "upstream_status": null
                  }
                }
              }
            }
          }
        }
      },
      "UpstreamClientError": {
        "description": "The provider answered with a client error, passed on unchanged (404, 408, 409, 422 and other 4xx). `upstream_status` carries the provider's status.",
        "headers": {
          "X-ZeroLog-Upstream-Status": {
            "$ref": "#/components/headers/UpstreamStatus"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": {
                    "message": "model_not_found",
                    "type": "not_found_error",
                    "upstream_status": 404
                  }
                }
              }
            }
          }
        }
      },
      "UpstreamRateLimited": {
        "description": "The provider is rate-limiting. Retry after the time in `Retry-After` when present.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-ZeroLog-Upstream-Status": {
            "$ref": "#/components/headers/UpstreamStatus"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": {
                    "message": "the model is temporarily rate-limited upstream",
                    "type": "rate_limit_error",
                    "upstream_status": 429
                  }
                }
              }
            }
          }
        }
      },
      "WebhookBadRequest": {
        "description": "The webhook's chat and container are no longer attached to each other (`not_attached`), or the body is not valid JSON (then without `code`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "that chat and container are not attached to each other",
                  "code": "not_attached"
                }
              }
            }
          }
        }
      },
      "WebhookConflict": {
        "description": "The container could not be resumed for the run.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "could not resume the container: no capacity",
                  "code": "resume_failed"
                }
              }
            }
          }
        }
      },
      "WebhookForbidden": {
        "description": "The account is disabled or lacks the goal mode permission.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "permission required: goal_mode (active account required)",
                  "code": "permission"
                }
              }
            }
          }
        }
      },
      "WebhookInternal": {
        "description": "Unexpected server error. The body never carries internal details.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "internal error"
                }
              }
            }
          }
        }
      },
      "WebhookMethodNotAllowed": {
        "description": "Any method other than POST.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "use POST to trigger a webhook"
                }
              }
            }
          }
        }
      },
      "WebhookNoFunds": {
        "description": "No prepaid credit left on the account.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "out of prepaid credit — top up to run this hook",
                  "code": "no_funds"
                }
              }
            }
          }
        }
      },
      "WebhookRateLimited": {
        "description": "Per-minute (`rate_min`) or daily (`rate_day`) limit of this webhook reached. Wait `retry_after_s` seconds.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "rate limit — too many triggers this minute",
                  "code": "rate_min",
                  "retry_after_s": 60
                }
              }
            }
          }
        }
      },
      "WebhookTooLarge": {
        "description": "Body larger than this webhook accepts (32 kB for goal webhooks, 256 kB for inject webhooks).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "request entity too large"
                }
              }
            }
          }
        }
      },
      "WebhookUnknown": {
        "description": "Unknown or revoked token (`no_hook`). A wrong token and a revoked one get the same answer, so tokens cannot be probed. `no_chat` and `no_container` mean the webhook still exists but its chat or container is gone.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/WebhookError"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "unknown or revoked hook",
                  "code": "no_hook"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "AuthError": {
        "type": "object",
        "required": [
          "type",
          "error"
        ],
        "properties": {
          "type": {
            "const": "error"
          },
          "error": {
            "type": "object",
            "required": [
              "type",
              "message"
            ],
            "properties": {
              "type": {
                "const": "authentication_error"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "ChatCompletion": {
        "type": "object",
        "required": [
          "id",
          "object",
          "model",
          "choices"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "chat.completion"
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string",
            "description": "The model that answered."
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "index",
                "message"
              ],
              "properties": {
                "index": {
                  "type": "integer"
                },
                "message": {
                  "type": "object",
                  "required": [
                    "role"
                  ],
                  "properties": {
                    "role": {
                      "const": "assistant"
                    },
                    "content": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "tool_calls": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "additionalProperties": true
                },
                "finish_reason": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "additionalProperties": true
            }
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        },
        "additionalProperties": true
      },
      "ChatCompletionRequest": {
        "type": "object",
        "required": [
          "messages"
        ],
        "properties": {
          "model": {
            "type": "string",
            "description": "Model id. Optional; without it the catalogue default is used."
          },
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChatMessage"
            }
          },
          "stream": {
            "type": "boolean",
            "default": false
          },
          "tools": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "OpenAI tool definitions, passed to the provider."
          },
          "tool_choice": {
            "description": "Passed to the provider."
          },
          "temperature": {
            "type": "number"
          },
          "max_tokens": {
            "type": "integer"
          }
        },
        "additionalProperties": true
      },
      "ChatMessage": {
        "type": "object",
        "required": [
          "role"
        ],
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "system",
              "developer",
              "user",
              "assistant",
              "tool"
            ]
          },
          "content": {
            "description": "Text, or an array of OpenAI content parts.",
            "type": [
              "string",
              "array",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "tool_calls": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "tool_call_id": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "ClientToken": {
        "type": "object",
        "required": [
          "token",
          "base_url"
        ],
        "properties": {
          "token": {
            "type": "string",
            "pattern": "^mhc-[0-9]+-\\S+$",
            "description": "Your client token. Keep it secret."
          },
          "base_url": {
            "type": "string",
            "format": "uri",
            "description": "Base URL including `/v1`."
          },
          "note": {
            "type": "string"
          }
        }
      },
      "CodedError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          }
        },
        "additionalProperties": true
      },
      "Error": {
        "type": "object",
        "description": "OpenAI-style error.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "message",
              "type"
            ],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "invalid_request_error",
                  "authentication_error",
                  "permission_error",
                  "not_found_error",
                  "billing_error",
                  "rate_limit_error",
                  "api_error"
                ]
              },
              "code": {
                "type": "string",
                "description": "Machine-readable reason, when there is one: `model_not_available`, `provider_temporarily_paused`, `provider_unavailable`, `no_key`, `byok_key_invalid` and others."
              },
              "upstream_status": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Status from the model provider; `null` when it did not answer."
              },
              "cause": {
                "type": "string",
                "description": "Why the fallback was tried, when it was."
              }
            }
          }
        }
      },
      "ErrorOrSimple": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "$ref": "#/components/schemas/SimpleError"
          }
        ]
      },
      "GoalHookAccepted": {
        "type": "object",
        "required": [
          "job_id",
          "status_url"
        ],
        "properties": {
          "job_id": {
            "type": "integer"
          },
          "status_url": {
            "type": "string",
            "description": "Job page in the app (needs a browser session)."
          }
        }
      },
      "GoalHookTrigger": {
        "type": "object",
        "properties": {
          "prompt": {
            "type": "string",
            "maxLength": 4000,
            "description": "Appended to the stored objective when the webhook is in append mode; ignored otherwise. Longer text is cut at 4000 characters."
          }
        },
        "additionalProperties": true
      },
      "InjectAccepted": {
        "type": "object",
        "required": [
          "ok",
          "status",
          "path"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "stored",
              "error"
            ]
          },
          "path": {
            "const": "/work/.webhooks/incoming.jsonl"
          }
        }
      },
      "Model": {
        "type": "object",
        "required": [
          "id",
          "object",
          "created",
          "owned_by"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Model id to send as `model`."
          },
          "object": {
            "const": "model"
          },
          "created": {
            "type": "integer"
          },
          "owned_by": {
            "const": "zerolog"
          }
        }
      },
      "ModelList": {
        "type": "object",
        "required": [
          "object",
          "data"
        ],
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Model"
            }
          }
        }
      },
      "SimpleError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "prompt_tokens": {
            "type": "integer"
          },
          "completion_tokens": {
            "type": "integer"
          },
          "total_tokens": {
            "type": "integer"
          }
        },
        "additionalProperties": true
      },
      "WebhookError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Machine-readable reason: `no_hook`, `no_chat`, `no_container`, `not_attached`, `permission`, `no_funds`, `rate_min`, `rate_day`, `resume_failed` or the code of the container error. Missing on 405 and on unreadable bodies."
          },
          "retry_after_s": {
            "type": "integer",
            "description": "Only on 429."
          }
        }
      }
    },
    "parameters": {
      "GoalHookToken": {
        "name": "token",
        "in": "path",
        "required": true,
        "description": "Webhook token, `mhw_` followed by 32 URL-safe characters.",
        "schema": {
          "type": "string",
          "examples": [
            "mhw_REPLACE_WITH_HOOK_TOKEN"
          ]
        }
      },
      "InjectToken": {
        "name": "token",
        "in": "path",
        "required": true,
        "description": "Webhook token, `mhin_` followed by 32 URL-safe characters.",
        "schema": {
          "type": "string",
          "examples": [
            "mhin_REPLACE_WITH_INJECT_TOKEN"
          ]
        }
      }
    },
    "headers": {
      "CacheControlPrivate": {
        "description": "`private, no-store` on JSON answers; `no-cache` on event streams.",
        "schema": {
          "type": "string",
          "enum": [
            "private, no-store",
            "no-cache"
          ]
        }
      },
      "ModelNotice": {
        "description": "Set only when another model answered: a readable sentence saying so.",
        "schema": {
          "type": "string"
        }
      },
      "ReroutedFrom": {
        "description": "Set only when another model answered: the model you asked for.",
        "schema": {
          "type": "string"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait, passed on from the provider when it sent one.",
        "schema": {
          "type": "string"
        }
      },
      "ServedModel": {
        "description": "Set only when another model answered: the model that did.",
        "schema": {
          "type": "string"
        }
      },
      "ServedProvider": {
        "description": "Set only when another model answered: its provider.",
        "schema": {
          "type": "string"
        }
      },
      "UpstreamStatus": {
        "description": "The HTTP status the model provider returned, when there was one.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9]{3}$"
        }
      }
    },
    "securitySchemes": {
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "The same client token in `x-api-key`, for clients that send keys this way. If both headers are present, `x-api-key` wins."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your client token (`mhc-<account>-<secret>`), from `GET /api/cli/token` or `zerolog login`. Send `Authorization: Bearer mhc-…`."
      },
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-mh_session",
        "description": "Browser session from signing in at zerolog.ch. Not for scripts."
      }
    }
  }
}
