{
  "openapi": "3.0.3",
  "info": {
    "title": "Hopscotch",
    "version": "v1",
    "description": "An OpenAI-compatible inference API. Point an OpenAI client at https://api.hopscotchlabs.ai, give it a Hopscotch API key, and call models across providers on one prepaid balance.\n\nEvery response, errors included, matches the shape an unmodified OpenAI SDK expects."
  },
  "servers": [
    {
      "url": "https://api.hopscotchlabs.ai",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Chat",
      "description": "Chat completions, streamed or not."
    },
    {
      "name": "Models",
      "description": "What you can call."
    }
  ],
  "paths": {
    "/v1/chat/completions": {
      "post": {
        "servers": [
          {
            "url": "https://api.hopscotchlabs.ai",
            "description": "Production"
          }
        ],
        "tags": [
          "Chat"
        ],
        "summary": "Create a chat completion",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "model": {
                    "type": "string",
                    "description": "The model to call, in one of exactly two forms. A provider-pinned id, \"{provider}/{that provider's own id}\", is split once on the FIRST slash: the part before it names one of the provider accounts this platform serves, and everything after it is that provider's own id for the model, passed on verbatim, slashes included. A workspace routing-profile slug, \"profile/{identifier}\", is the other form: the platform then tries that profile's models in the customer's own order, and the usage record and the charge name whichever model actually served. A bare model id is refused with 400 and the code \"model_id_not_pinned\". GET /v1/models lists the exact strings this key can call, one per model and provider that serves it. A third string is accepted alongside them: \"hopscotch/auto\", which names whichever routing profile the workspace made its default. It is a fixed id every workspace can send, so a client is configured once and the route behind it is changed later on the Routing screen without touching the calling code. It resolves through the same path a profile slug takes, so the usage record and the charge name whichever model actually served, and a workspace with no default route set is refused 404 \"model_not_available\" saying exactly that.",
                    "example": "openai/gpt-4o-mini"
                  },
                  "messages": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "max_tokens": {
                    "type": "integer",
                    "description": "The most output tokens the reply may use. Optional, as it is on OpenAI. Left out, it is filled in for the providers whose own API refuses a request that does not carry one, and the figure used is the model's published max_output_tokens, which GET /v1/models/{model} reports and which is what credit is held for on a request naming no ceiling. Every other provider is sent the request unchanged. max_completion_tokens counts as this field being given.",
                    "example": 256
                  },
                  "stream": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "model",
                  "messages"
                ],
                "$ref": "#/components/schemas/ChatCompletionRequest"
              },
              "example": {
                "model": "openai/gpt-4o-mini",
                "messages": [
                  {
                    "role": "user",
                    "content": "Say hi in five words."
                  }
                ]
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The completion. A response that is not streamed carries the provider's token counts in its own `usage` object. Its `model` field echoes the slug you asked for rather than the provider's internal name, except in three cases that are returned as the provider sent them: a streamed reply, whose frames are passed through unchanged; a request naming a routing profile, which names a chain rather than one model; and a successful body carrying no string `model` field at all.\n\nWhen `stream` is true the body is `text/event-stream` instead, and the counts arrive on a `:x-hopscotch-usage` comment line immediately before `data: [DONE]`; the server-sent events specification requires parsers to ignore comment lines, so an SDK never sees it.",
            "headers": {
              "x-hopscotch-served-by": {
                "description": "The full id, \"{provider}/{model}\", of the model that actually answered: the pinned id when the request named one directly, or the routing-profile chain member that served when the request named a profile.",
                "schema": {
                  "type": "string",
                  "example": "openai/gpt-4o-mini"
                }
              },
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "x-request-id": {
                "$ref": "#/components/headers/SdkRequestId"
              },
              "x-hopscotch-provider": {
                "$ref": "#/components/headers/Provider"
              },
              "x-hopscotch-retry-attempt-count": {
                "$ref": "#/components/headers/RetryAttemptCount"
              },
              "x-hopscotch-cache-status": {
                "$ref": "#/components/headers/CacheStatus"
              },
              "x-hopscotch-trace-id": {
                "$ref": "#/components/headers/TraceId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatCompletion"
                },
                "example": {
                  "id": "chatcmpl-EXAMPLE",
                  "object": "chat.completion",
                  "created": 1756080000,
                  "model": "openai/gpt-4o-mini",
                  "choices": [
                    {
                      "index": 0,
                      "message": {
                        "role": "assistant",
                        "content": "Hello, good to meet you."
                      },
                      "finish_reason": "stop"
                    }
                  ],
                  "usage": {
                    "prompt_tokens": 12,
                    "completion_tokens": 8,
                    "total_tokens": 20
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request cannot be served as sent. `code` is null for a missing parameter, with `param` naming it, `context_length_exceeded` for a request too large to price or serve in one call, and `provider_key_refused` when the request ran on your own key for a provider that rejected it or refused this particular call under it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_model": {
                    "summary": "No model named",
                    "value": {
                      "error": {
                        "message": "You must provide a model parameter naming the model to call. It is read to work out what the request could cost before it is sent.",
                        "type": "invalid_request_error",
                        "param": "model",
                        "code": null,
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "context_length_exceeded": {
                    "summary": "Too large to serve in one call",
                    "value": {
                      "error": {
                        "message": "This request is too large to price: it is estimated at 4000000 prompt tokens, which is beyond anything this platform can hold credit for. Send less in one call.",
                        "type": "invalid_request_error",
                        "param": "messages",
                        "code": "context_length_exceeded",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "provider_key_refused": {
                    "summary": "Your own provider key was rejected",
                    "value": {
                      "error": {
                        "message": "openai rejected your workspace's own openai key (401), so the credential itself was not accepted. Replace it on the Provider keys screen. While your workspace holds a key for openai, every request to openai runs on yours and none is sent on a Hopscotch key, so no other credential was tried for it. Nothing was charged for this request. Remove the key on the Provider keys screen and these requests go back to running on Hopscotch's key and being charged for.",
                        "type": "invalid_request_error",
                        "param": null,
                        "code": "provider_key_refused",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "No key, a scheme other than Bearer, a key of the wrong shape, or a key we reject. An unknown key and a revoked key are answered with one identical message on purpose.\n\nA key past its own expiry date is refused here too, with code `key_expired` and a message naming the date. It is checked before the per-minute rate limiter, so an expired key never consumes that allowance. Expired keys are not renewed: create a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Incorrect API key provided.",
                    "type": "authentication_error",
                    "param": null,
                    "code": "invalid_api_key",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "The request will not be paid for, and nothing was charged. Code `insufficient_credit` is the workspace being out of credit, which stops every key on it. Code `key_limit_exceeded` is the sending key reaching its own credit limit, which leaves every other key in the workspace serving normally; it is refused before any credit is held, and the message names what that key has settled and holds inside its current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "insufficient_credit": {
                    "summary": "The workspace is out of credit",
                    "value": {
                      "error": {
                        "message": "This account has 54 minor units available and the request needs 55 held.",
                        "type": "insufficient_quota",
                        "param": null,
                        "code": "insufficient_credit",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "key_limit_exceeded": {
                    "summary": "This key is at its own limit",
                    "value": {
                      "error": {
                        "message": "This key may spend $20.00 in its current window and has $18.40 settled and $1.70 held against that, so a request needing $0.30 more is refused. Nothing was charged for it and no credit was held. The limit belongs to this key alone, so other keys in the workspace are unaffected. This limit resets monthly on UTC calendar windows, so requests are admitted again when the window rolls. It can also be raised or removed on the Keys screen.",
                        "type": "insufficient_quota",
                        "param": null,
                        "code": "key_limit_exceeded",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "This workspace is not permitted to call what the request named, and code is `model_not_permitted`. For a model the message names that model. For a routing profile the message names the slug and no member, and it is answered when the permitted set excludes every member that could otherwise have been served, including a profile that also holds a member we could not serve ourselves. The permitted set is unrestricted by default, so this is reachable only on a workspace somebody has restricted. No provider was called and retrying will not change the answer: a workspace owner has to widen the set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "model_not_permitted": {
                    "summary": "The model is outside this workspace's permitted set",
                    "value": {
                      "error": {
                        "message": "This workspace is not permitted to use the model \"MODEL_ID\". Its access is limited to a named set of models; a workspace owner can say which.",
                        "type": "permission_error",
                        "param": null,
                        "code": "model_not_permitted",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "model_not_permitted_profile": {
                    "summary": "A routing profile has no permitted member left to try",
                    "value": {
                      "error": {
                        "message": "Every model in \"PROFILE_SLUG\" that could be served is outside this workspace's permitted set, so the profile has nothing left to try. Its access is limited to a named set of models; a workspace owner can say which.",
                        "type": "permission_error",
                        "param": null,
                        "code": "model_not_permitted",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "The model cannot be served, and the two cases are told apart by `code`. `model_not_found` means the id is in no catalog at all, which is what a typo produces, so check the spelling or list what this key can call with `GET /v1/models`. `model_not_available` means the id is catalogued and is not being served right now, either on this endpoint or at all, so retrying later is worth more than changing the request. No provider was called either way.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "model_not_found": {
                    "summary": "No model of that id exists",
                    "value": {
                      "error": {
                        "message": "The model \"MODEL_ID\" does not exist. Check the spelling, or list the models this key can call with GET /v1/models.",
                        "type": "not_found_error",
                        "param": null,
                        "code": "model_not_found",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "model_not_available": {
                    "summary": "Known to us, and not being served right now",
                    "value": {
                      "error": {
                        "message": "The model \"MODEL_ID\" is not currently available. It is known to this platform and is not being served right now, so retrying later is worth more than changing the request.",
                        "type": "not_found_error",
                        "param": null,
                        "code": "model_not_available",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "model_not_available_endpoint": {
                    "summary": "Live, and not offered on this endpoint",
                    "value": {
                      "error": {
                        "message": "\"MODEL_ID\" is not offered on this endpoint: the catalog lists it for chat and not for embeddings.",
                        "type": "not_found_error",
                        "param": null,
                        "code": "model_not_available",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "413": {
            "description": "The request body is larger than the 8388608 byte maximum this API accepts, so it was refused at the door. A body that declares its length is refused without a byte being read; one that declares none is read only as far as the ceiling and then abandoned. Nothing was parsed, no credit was held, no model was called and nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "payload_too_large": {
                    "summary": "The request body is over the ceiling",
                    "value": {
                      "error": {
                        "message": "This request body is larger than the 8388608 byte maximum this API accepts. Nothing was read, no model was called and nothing was charged. Send fewer messages, a shorter prompt, or fewer inputs.",
                        "type": "invalid_request_error",
                        "param": null,
                        "code": "payload_too_large",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "A limit refused the request. `rate_limit_exceeded` is this key's own request rate and carries `Retry-After`. `spend_rate_exceeded` is the account spend rate cap, which counts the credit held for requests rather than the credit they settle at; send `max_tokens` to hold less. `provider_key_refused` is your own key for a provider being over its limit there; it clears on its own and carries no `Retry-After`. `upstream_rate_limited` is the model's provider rate-limiting the call: nothing about the request is wrong, nothing is charged, and it carries `Retry-After`.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer",
                  "minimum": 1
                },
                "description": "Seconds to wait. Present on `rate_limit_exceeded` and on `upstream_rate_limited`.",
                "example": 17
              },
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "rate_limit_exceeded": {
                    "summary": "This key's request rate",
                    "value": {
                      "error": {
                        "message": "Rate limit reached for this key. Try again in 12 seconds. Request id REQUEST_ID.",
                        "type": "rate_limit_error",
                        "param": null,
                        "code": "rate_limit_exceeded",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "spend_rate_exceeded": {
                    "summary": "The account spend rate cap",
                    "value": {
                      "error": {
                        "message": "This account has already reserved 5000 minor units in the last 60 seconds, which is at its ceiling of 5000. Requests are admitted again within 60 seconds of the earliest reservation counted, with nothing to do in the meantime.",
                        "type": "rate_limit_error",
                        "param": null,
                        "code": "spend_rate_exceeded",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "provider_key_refused": {
                    "summary": "Your own provider key is over its limit",
                    "value": {
                      "error": {
                        "message": "openai refused this request as over its limits, on your workspace's own openai key. This is your account's limit at openai rather than ours. While your workspace holds a key for openai, every request to openai runs on yours and none is sent on a Hopscotch key, so no other credential was tried for it. Nothing was charged for this request. Remove the key on the Provider keys screen and these requests go back to running on Hopscotch's key and being charged for.",
                        "type": "rate_limit_error",
                        "param": null,
                        "code": "provider_key_refused",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "upstream_rate_limited": {
                    "summary": "The model's provider rate-limited the call",
                    "value": {
                      "error": {
                        "message": "The server had an error while processing your request. Sorry about that! Quote the request id if you contact support.",
                        "type": "rate_limit_error",
                        "param": null,
                        "code": "upstream_rate_limited",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            }
          },
          "529": {
            "description": "The model's provider answered `529` rather than `429`. Carries the same `upstream_rate_limited` code as the 429 case, with `type` `api_error` as the status class requires: nothing about the request is wrong, nothing is charged, and it carries `Retry-After`.",
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "schema": {
                  "type": "integer",
                  "minimum": 1
                },
                "description": "Seconds to wait. Present the same way as on the 429 `upstream_rate_limited`.",
                "example": 17
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "upstream_rate_limited": {
                    "summary": "The model's provider rate-limited the call, at 529",
                    "value": {
                      "error": {
                        "message": "The server had an error while processing your request. Sorry about that! Quote the request id if you contact support.",
                        "type": "api_error",
                        "param": null,
                        "code": "upstream_rate_limited",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "A fault on our side. The request id is the one thing worth keeping.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "The server had an error while processing your request. Sorry about that! Quote the request id if you contact support.",
                    "type": "api_error",
                    "param": null,
                    "code": null,
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "503": {
            "description": "Ours rather than yours, and `code` names the condition. `paused_by_owner` clears only when somebody lifts the pause, so retrying on a timer will not help. `balance_unavailable` (we could not read the balance, so the request was refused rather than served unmetered), `model_not_priceable` and `model_pricing_unreadable` (a model we list could not be priced for this call) and `no_provider_configured` (we are not currently configured to serve this) all clear without you doing anything.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "paused_by_owner": {
                    "summary": "Somebody pressed Pause",
                    "value": {
                      "error": {
                        "message": "Every request on this account is paused, because somebody with access to it pressed Pause in the dashboard. Nothing was charged for this request and no credit was held for it. It is retryable, and it will be refused the same way until the pause is lifted, so retrying on a timer will not clear it. Requests start working again the moment the pause is lifted from the same control, with nothing else to do and nothing to re-create.",
                        "type": "server_error",
                        "param": null,
                        "code": "paused_by_owner",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "balance_unavailable": {
                    "summary": "We could not read the balance",
                    "value": {
                      "error": {
                        "message": "Your credit balance could not be checked while this request was running, so it was stopped rather than left unwatched. This is a fault on our side rather than a problem with your account, and it does not mean you are out of credit. Try again shortly.",
                        "type": "server_error",
                        "param": null,
                        "code": "balance_unavailable",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "504": {
            "description": "The request ran past the request deadline and was terminated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "This request ran longer than the 540 second limit and was terminated. Reduce the amount of work in one call, or stream the response, and try again.",
                    "type": "server_error",
                    "param": null,
                    "code": "request_deadline_exceeded",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "description": "Send an OpenAI chat completion request. Set `stream: true` for server-sent events.\n\nCredit is held when the request is admitted and settled against the provider's own token counts when it finishes. You are charged the provider's rate at face value. Nothing is added per request.",
        "security": [
          {
            "customerApiKey": []
          }
        ],
        "operationId": "createChatCompletion"
      }
    },
    "/v1/models": {
      "get": {
        "servers": [
          {
            "url": "https://api.hopscotchlabs.ai",
            "description": "Production"
          }
        ],
        "tags": [
          "Models"
        ],
        "summary": "List models",
        "description": "List the models you can call. The `id` of each entry is what you send as `model`.\n\nEach entry is the OpenAI model object with a `hopscotch` block beside it, carrying what OpenAI's own object has no field for: who serves the model, what it can do, its context window, and the provider's list price.",
        "responses": {
          "200": {
            "description": "The models available to your key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelList"
                },
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "openai/gpt-4o-mini",
                      "object": "model",
                      "created": 1756080000,
                      "owned_by": "openai",
                      "hopscotch": {
                        "model_id": "openai/gpt-4o-mini",
                        "display_name": "GPT-4o mini",
                        "author": "openai",
                        "context_window_tokens": 128000,
                        "max_output_tokens": 16384,
                        "input_rate_thousandths": 150,
                        "output_rate_thousandths": 600,
                        "currency_code": "USD",
                        "endpoint_families": [
                          "chat_completions"
                        ],
                        "provider_display_names": [
                          "OpenAI"
                        ],
                        "capabilities": {
                          "audio": "no",
                          "json_mode": "yes",
                          "streaming": "yes",
                          "tools": "yes",
                          "vision": "yes"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "No key, a scheme other than Bearer, a key of the wrong shape, a key we reject, or a key past its own expiry date, which carries code `key_expired` and names the date it expired on.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Incorrect API key format. A Hopscotch key looks like ub_live_ followed by 43 characters. Check you have not pasted a key from another provider.",
                    "type": "authentication_error",
                    "param": null,
                    "code": "invalid_api_key",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "405": {
            "description": "A method other than GET on a path that exists. The route boundary answers this before any key is looked at, so it costs nothing and reveals nothing.",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "GET"
                  ]
                },
                "description": "The methods this path accepts.",
                "example": "GET"
              },
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "POST is not supported for /v1/models in this release.",
                    "type": "invalid_request_error",
                    "param": null,
                    "code": "method_not_allowed",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            }
          },
          "429": {
            "description": "This key's own request rate limit. Every route on this API is subject to it.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer",
                  "minimum": 1
                },
                "example": 17,
                "description": "Seconds to wait."
              },
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Rate limit reached for this key. Try again in 12 seconds. Request id REQUEST_ID.",
                    "type": "rate_limit_error",
                    "param": null,
                    "code": "rate_limit_exceeded",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            }
          },
          "500": {
            "description": "A fault on our side. The request id is the one thing worth keeping.",
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "The server had an error while processing your request. Sorry about that! Quote the request id if you contact support.",
                    "type": "api_error",
                    "param": null,
                    "code": null,
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            }
          },
          "503": {
            "description": "We are not currently configured to serve a model list. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "No provider is configured, so there is nothing to serve this request.",
                    "type": "api_error",
                    "param": null,
                    "code": "no_provider_configured",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "security": [
          {
            "customerApiKey": []
          }
        ],
        "operationId": "listModels"
      }
    },
    "/v1/models/{id}": {
      "get": {
        "servers": [
          {
            "url": "https://api.hopscotchlabs.ai",
            "description": "Production"
          }
        ],
        "tags": [
          "Models"
        ],
        "summary": "Retrieve a model",
        "description": "Everything the list carries for one model, plus the fields served only here: the model's own description, the request parameters it accepts, and its deprecation and sunset dates where it has them.\n\nA model id contains slashes and the whole of it is the identifier. Percent-encode it once, or send it unencoded; a double-encoded slash names no model and answers 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The model id, exactly as the list gives it.",
            "schema": {
              "type": "string"
            },
            "example": "openai/gpt-4o-mini"
          }
        ],
        "responses": {
          "200": {
            "description": "The model.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelDetail"
                },
                "example": {
                  "id": "openai/gpt-4o-mini",
                  "object": "model",
                  "created": 1756080000,
                  "owned_by": "openai",
                  "hopscotch": {
                    "model_id": "openai/gpt-4o-mini",
                    "display_name": "GPT-4o mini",
                    "author": "openai",
                    "author_slug": "meta",
                    "author_source": "recorded",
                    "context_window_tokens": 128000,
                    "max_output_tokens": 16384,
                    "input_rate_thousandths": 150,
                    "output_rate_thousandths": 600,
                    "currency_code": "USD",
                    "endpoint_families": [
                      "chat_completions"
                    ],
                    "provider_display_names": [
                      "OpenAI"
                    ],
                    "provider_families": [
                      "groq"
                    ],
                    "capabilities": {
                      "streaming": "yes",
                      "tools": "yes",
                      "json_mode": "yes",
                      "vision": "yes",
                      "audio": "no"
                    },
                    "parameters": [
                      {
                        "name": "temperature",
                        "type": "number",
                        "min_milli": 0,
                        "max_milli": 2000,
                        "default_milli": 1000,
                        "enum_values": null,
                        "sent_by_default": false,
                        "note": "Higher values make the reply less predictable."
                      }
                    ],
                    "deprecated_at": null,
                    "sunset_at": null,
                    "replacement_model_id": null,
                    "description": "A small, fast model for everyday chat and extraction work."
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "No key, a scheme other than Bearer, a key of the wrong shape, a key we reject, or a key past its own expiry date, which carries code `key_expired` and names the date it expired on.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Incorrect API key provided.",
                    "type": "authentication_error",
                    "param": null,
                    "code": "invalid_api_key",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "No model of that id is in the live catalog. A model we hold but do not publish answers the same way as one we have never heard of, on purpose.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "The model \"openai/gpt-4o-mini\" was not found in the live catalog.",
                    "type": "not_found_error",
                    "param": "model",
                    "code": "model_not_found",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "405": {
            "description": "A method other than GET on a path that exists. The route boundary answers this before any key is looked at, so it costs nothing and reveals nothing.",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "GET"
                  ]
                },
                "description": "The methods this path accepts.",
                "example": "GET"
              },
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "POST is not supported for /v1/models/openai/gpt-4o-mini in this release.",
                    "type": "invalid_request_error",
                    "param": null,
                    "code": "method_not_allowed",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            }
          },
          "429": {
            "description": "This key's own request rate limit. Every route on this API is subject to it.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer",
                  "minimum": 1
                },
                "example": 17,
                "description": "Seconds to wait."
              },
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Rate limit reached for this key. Try again in 12 seconds. Request id REQUEST_ID.",
                    "type": "rate_limit_error",
                    "param": null,
                    "code": "rate_limit_exceeded",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            }
          },
          "500": {
            "description": "A fault on our side. The request id is the one thing worth keeping.",
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "The server had an error while processing your request. Sorry about that! Quote the request id if you contact support.",
                    "type": "api_error",
                    "param": null,
                    "code": null,
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            }
          },
          "503": {
            "description": "The catalog could not be served. Nothing is wrong with the request; retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "No provider is configured, so there is nothing to serve this request.",
                    "type": "api_error",
                    "param": null,
                    "code": "no_provider_configured",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "security": [
          {
            "customerApiKey": []
          }
        ],
        "operationId": "retrieveModel"
      }
    },
    "/v1/responses": {
      "post": {
        "servers": [
          {
            "url": "https://api.hopscotchlabs.ai",
            "description": "Production"
          }
        ],
        "tags": [
          "Responses"
        ],
        "summary": "Create a model response",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "model": {
                    "type": "string",
                    "description": "The model to call, which the live catalog has to offer for the responses family. A provider-pinned id, `{provider}/{that provider's own id}`, is split once on the first slash. A routing-profile slug, `profile/{identifier}`, tries that profile's models in your own order. `hopscotch/auto` names whichever route your workspace made its default. A bare model id is refused with 400 and the code `model_id_not_pinned`. `GET /v1/models` lists the exact strings this key can call.",
                    "example": "openai/gpt-4o-mini"
                  },
                  "input": {
                    "description": "The Responses API input, forwarded opaquely: a string, or the array of typed input items the API accepts.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "object"
                        }
                      }
                    ]
                  },
                  "stream": {
                    "type": "boolean",
                    "description": "Server-sent events instead of one JSON body."
                  },
                  "store": {
                    "type": "boolean",
                    "description": "This platform stores no response, so only `false` is accepted. Omitting the field is served and nothing is forwarded in its place, so the provider applies its own default. An explicit `true` is refused before admission, naming this field.",
                    "enum": [
                      false
                    ]
                  }
                },
                "required": [
                  "model",
                  "input"
                ]
              },
              "example": {
                "model": "openai/gpt-4o-mini",
                "input": "Say hi in five words."
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The model response, in the Responses format, forwarded as the provider sent it.\n\nWhen `stream` is true the body is `text/event-stream` instead, and the token counts arrive on a `:x-hopscotch-usage` comment line immediately before the stream ends; the server-sent events specification requires parsers to ignore comment lines, so an SDK never sees it.",
            "headers": {
              "x-hopscotch-served-by": {
                "description": "The full id, \"{provider}/{model}\", of the model that actually answered: the pinned id when the request named one directly, or the routing-profile chain member that served when the request named a profile.",
                "schema": {
                  "type": "string",
                  "example": "openai/gpt-4o-mini"
                }
              },
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "x-request-id": {
                "$ref": "#/components/headers/SdkRequestId"
              },
              "x-hopscotch-provider": {
                "$ref": "#/components/headers/Provider"
              },
              "x-hopscotch-retry-attempt-count": {
                "$ref": "#/components/headers/RetryAttemptCount"
              },
              "x-hopscotch-cache-status": {
                "$ref": "#/components/headers/CacheStatus"
              },
              "x-hopscotch-trace-id": {
                "$ref": "#/components/headers/TraceId"
              }
            }
          },
          "400": {
            "description": "The request cannot be served as sent. `unsupported_value` with `param` set to `store` is this endpoint's own refusal and is answered before admission, so nothing is held and no provider is called. `code` is null for a missing parameter, with `param` naming it. A refusal the provider made about this request is relayed with the provider's own status and its own sentence, quoted after the provider's name, so a parameter it does not accept reads as what it is rather than as a fault on this platform.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "store_true": {
                    "summary": "Asked us to store the response",
                    "value": {
                      "error": {
                        "message": "This platform does not store responses, so `store: true` cannot be honoured. Send `store: false`, or omit the field.",
                        "type": "invalid_request_error",
                        "param": "store",
                        "code": "unsupported_value",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "missing_model": {
                    "summary": "No model named",
                    "value": {
                      "error": {
                        "message": "You must provide a model parameter naming the model to call. It is read to work out what the request could cost before it is sent.",
                        "type": "invalid_request_error",
                        "param": "model",
                        "code": null,
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "provider_key_refused": {
                    "summary": "Your own provider key was rejected",
                    "value": {
                      "error": {
                        "message": "openai rejected your workspace's own openai key (401), so the credential itself was not accepted. Replace it on the Provider keys screen. While your workspace holds a key for openai, every request to openai runs on yours and none is sent on a Hopscotch key, so no other credential was tried for it. Nothing was charged for this request. Remove the key on the Provider keys screen and these requests go back to running on Hopscotch's key and being charged for.",
                        "type": "invalid_request_error",
                        "param": null,
                        "code": "provider_key_refused",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "401": {
            "description": "No key, a scheme other than Bearer, a key of the wrong shape, or a key we reject. An unknown key and a revoked key are answered with one identical message on purpose.\n\nA key past its own expiry date is refused here too, with code `key_expired` and a message naming the date. It is checked before the per-minute rate limiter, so an expired key never consumes that allowance. Expired keys are not renewed: create a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Incorrect API key provided.",
                    "type": "authentication_error",
                    "param": null,
                    "code": "invalid_api_key",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "402": {
            "description": "The request will not be paid for, and nothing was charged. Code `insufficient_credit` is the workspace being out of credit, which stops every key on it. Code `key_limit_exceeded` is the sending key reaching its own credit limit, which leaves every other key in the workspace serving normally; it is refused before any credit is held, and the message names what that key has settled and holds inside its current window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "insufficient_credit": {
                    "summary": "The workspace is out of credit",
                    "value": {
                      "error": {
                        "message": "This account has 54 minor units available and the request needs 55 held.",
                        "type": "insufficient_quota",
                        "param": null,
                        "code": "insufficient_credit",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "key_limit_exceeded": {
                    "summary": "This key is at its own limit",
                    "value": {
                      "error": {
                        "message": "This key may spend $20.00 in its current window and has $18.40 settled and $1.70 held against that, so a request needing $0.30 more is refused. Nothing was charged for it and no credit was held. The limit belongs to this key alone, so other keys in the workspace are unaffected. This limit resets monthly on UTC calendar windows, so requests are admitted again when the window rolls. It can also be raised or removed on the Keys screen.",
                        "type": "insufficient_quota",
                        "param": null,
                        "code": "key_limit_exceeded",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "403": {
            "description": "This workspace is not permitted to call what the request named, and code is `model_not_permitted`. For a model the message names that model. For a routing profile the message names the slug and no member, and it is answered when the permitted set excludes every member that could otherwise have been served, including a profile that also holds a member we could not serve ourselves. The permitted set is unrestricted by default, so this is reachable only on a workspace somebody has restricted. No provider was called and retrying will not change the answer: a workspace owner has to widen the set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "model_not_permitted": {
                    "summary": "The model is outside this workspace's permitted set",
                    "value": {
                      "error": {
                        "message": "This workspace is not permitted to use the model \"MODEL_ID\". Its access is limited to a named set of models; a workspace owner can say which.",
                        "type": "permission_error",
                        "param": null,
                        "code": "model_not_permitted",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "model_not_permitted_profile": {
                    "summary": "A routing profile has no permitted member left to try",
                    "value": {
                      "error": {
                        "message": "Every model in \"PROFILE_SLUG\" that could be served is outside this workspace's permitted set, so the profile has nothing left to try. Its access is limited to a named set of models; a workspace owner can say which.",
                        "type": "permission_error",
                        "param": null,
                        "code": "model_not_permitted",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "404": {
            "description": "The model cannot be served, and the two cases are told apart by `code`. `model_not_found` means the id is in no catalog at all, which is what a typo produces, so check the spelling or list what this key can call with `GET /v1/models`. `model_not_available` means the id is catalogued and is not being served right now, either on this endpoint or at all, so retrying later is worth more than changing the request. No provider was called either way.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "model_not_found": {
                    "summary": "No model of that id exists",
                    "value": {
                      "error": {
                        "message": "The model \"MODEL_ID\" does not exist. Check the spelling, or list the models this key can call with GET /v1/models.",
                        "type": "not_found_error",
                        "param": null,
                        "code": "model_not_found",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "model_not_available": {
                    "summary": "Known to us, and not being served right now",
                    "value": {
                      "error": {
                        "message": "The model \"MODEL_ID\" is not currently available. It is known to this platform and is not being served right now, so retrying later is worth more than changing the request.",
                        "type": "not_found_error",
                        "param": null,
                        "code": "model_not_available",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "model_not_available_endpoint": {
                    "summary": "Live, and not offered on this endpoint",
                    "value": {
                      "error": {
                        "message": "\"MODEL_ID\" is not offered on this endpoint: the catalog lists it for chat and not for embeddings.",
                        "type": "not_found_error",
                        "param": null,
                        "code": "model_not_available",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "429": {
            "description": "A limit refused the request. `rate_limit_exceeded` is this key's own request rate and carries `Retry-After`. `spend_rate_exceeded` is the account spend rate cap, which counts the credit held for requests rather than the credit they settle at; send `max_tokens` to hold less. `provider_key_refused` is your own key for a provider being over its limit there; it clears on its own and carries no `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "rate_limit_exceeded": {
                    "summary": "This key's request rate",
                    "value": {
                      "error": {
                        "message": "Rate limit reached for this key. Try again in 12 seconds. Request id REQUEST_ID.",
                        "type": "rate_limit_error",
                        "param": null,
                        "code": "rate_limit_exceeded",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "spend_rate_exceeded": {
                    "summary": "The account spend rate cap",
                    "value": {
                      "error": {
                        "message": "This account has already reserved 5000 minor units in the last 60 seconds, which is at its ceiling of 5000. Requests are admitted again within 60 seconds of the earliest reservation counted, with nothing to do in the meantime.",
                        "type": "rate_limit_error",
                        "param": null,
                        "code": "spend_rate_exceeded",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "provider_key_refused": {
                    "summary": "Your own provider key is over its limit",
                    "value": {
                      "error": {
                        "message": "openai refused this request as over its limits, on your workspace's own openai key. This is your account's limit at openai rather than ours. While your workspace holds a key for openai, every request to openai runs on yours and none is sent on a Hopscotch key, so no other credential was tried for it. Nothing was charged for this request. Remove the key on the Provider keys screen and these requests go back to running on Hopscotch's key and being charged for.",
                        "type": "rate_limit_error",
                        "param": null,
                        "code": "provider_key_refused",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "description": "Seconds to wait. Present on `rate_limit_exceeded`.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                },
                "example": 17
              }
            }
          },
          "500": {
            "description": "A fault on our side. The request id is the one thing worth keeping.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "The server had an error while processing your request. Sorry about that! Quote the request id if you contact support.",
                    "type": "api_error",
                    "param": null,
                    "code": null,
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "503": {
            "description": "Ours rather than yours, and `code` names the condition. `paused_by_owner` clears only when somebody lifts the pause, so retrying on a timer will not help. `balance_unavailable` (we could not read the balance, so the request was refused rather than served unmetered), `model_not_priceable` and `model_pricing_unreadable` (a model we list could not be priced for this call) and `no_provider_configured` (we are not currently configured to serve this) all clear without you doing anything.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "paused_by_owner": {
                    "summary": "Somebody pressed Pause",
                    "value": {
                      "error": {
                        "message": "Every request on this account is paused, because somebody with access to it pressed Pause in the dashboard. Nothing was charged for this request and no credit was held for it. It is retryable, and it will be refused the same way until the pause is lifted, so retrying on a timer will not clear it. Requests start working again the moment the pause is lifted from the same control, with nothing else to do and nothing to re-create.",
                        "type": "server_error",
                        "param": null,
                        "code": "paused_by_owner",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  },
                  "balance_unavailable": {
                    "summary": "We could not read the balance",
                    "value": {
                      "error": {
                        "message": "Your credit balance could not be checked while this request was running, so it was stopped rather than left unwatched. This is a fault on our side rather than a problem with your account, and it does not mean you are out of credit. Try again shortly.",
                        "type": "server_error",
                        "param": null,
                        "code": "balance_unavailable",
                        "request_id": "REQUEST_ID"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          },
          "504": {
            "description": "The request ran past the request deadline and was terminated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "This request ran longer than the 540 second limit and was terminated. Reduce the amount of work in one call, or stream the response, and try again.",
                    "type": "server_error",
                    "param": null,
                    "code": "request_deadline_exceeded",
                    "request_id": "REQUEST_ID"
                  }
                }
              }
            },
            "headers": {
              "x-hopscotch-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            }
          }
        },
        "description": "Send a request in the OpenAI Responses format. Set `stream: true` for server-sent events.\n\nThis is the same door as chat completions: the same key, the same catalog, the same hold against your balance and the same usage row, filed under its own endpoint family so a charge is recognisable. The body is forwarded as you sent it, with one exception documented on `store` below.\n\nCredit is held when the request is admitted and settled against the provider's own token counts when it finishes. You are charged the provider's rate at face value. Nothing is added per request.",
        "security": [
          {
            "customerApiKey": []
          }
        ],
        "operationId": "createResponse"
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The error envelope. One top-level `error` key, always, at every status.",
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "message",
              "type",
              "param",
              "code"
            ],
            "additionalProperties": false,
            "properties": {
              "message": {
                "type": "string",
                "description": "Prose for a person. It may be reworded, so do not branch on it."
              },
              "type": {
                "type": "string",
                "description": "The category an SDK branches on. A closed set.",
                "enum": [
                  "invalid_request_error",
                  "authentication_error",
                  "permission_error",
                  "not_found_error",
                  "rate_limit_error",
                  "api_error",
                  "insufficient_quota",
                  "server_error"
                ]
              },
              "param": {
                "type": "string",
                "nullable": true,
                "description": "The request field at fault, or null. Present and null rather than absent."
              },
              "code": {
                "type": "string",
                "nullable": true,
                "description": "The stable machine-readable reason. Branch on this."
              },
              "request_id": {
                "type": "string",
                "description": "Our id for this request, the same value as the x-hopscotch-request-id header. Quote it when you contact support."
              }
            }
          }
        }
      },
      "ChatCompletionRequest": {
        "type": "object",
        "required": [
          "model",
          "messages"
        ],
        "additionalProperties": true,
        "description": "An OpenAI chat completion request. Fields beyond the ones named here are forwarded to the provider unchanged rather than refused for being unfamiliar.",
        "properties": {
          "model": {
            "type": "string",
            "description": "A model id in `family/model` form. List what you can call with GET /v1/models.",
            "example": "openai/gpt-4o-mini"
          },
          "messages": {
            "type": "array",
            "description": "The conversation so far, in OpenAI message form.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "stream": {
            "type": "boolean",
            "description": "Return the reply as server-sent events instead of one JSON body.",
            "default": false
          },
          "max_tokens": {
            "type": "integer",
            "minimum": 1,
            "description": "The most output tokens you will accept. Sending it also lowers the credit held for the request while it runs, so it is the lever that keeps a request under your account spend rate cap. Omit it and the model's published maximum output is held instead."
          },
          "stream_options": {
            "type": "object",
            "additionalProperties": true,
            "description": "OpenAI stream options. Set `include_usage` to receive OpenAI's own usage chunk in the stream. Hopscotch's own counts arrive separately on the :x-hopscotch-usage comment line either way.",
            "properties": {
              "include_usage": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "ChatCompletion": {
        "type": "object",
        "description": "An OpenAI chat completion, as the provider produced it.",
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "example": "chat.completion"
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string"
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "usage": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "ModelList": {
        "type": "object",
        "description": "An OpenAI model list.",
        "additionalProperties": true,
        "properties": {
          "object": {
            "type": "string",
            "example": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Model"
            }
          }
        }
      },
      "Model": {
        "type": "object",
        "description": "One model you may call.",
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "string",
            "description": "The value to send as `model`.",
            "example": "openai/gpt-4o-mini"
          },
          "object": {
            "type": "string",
            "example": "model"
          },
          "created": {
            "type": "integer",
            "description": "A unix timestamp in seconds. The release time where one is recorded and the record's own creation time otherwise, so it is not a release date for most of the list."
          },
          "owned_by": {
            "type": "string"
          },
          "shutdown_date": {
            "type": "integer",
            "description": "When the model stops serving, in unix seconds. Absent when there is none, rather than null."
          },
          "hopscotch": {
            "$ref": "#/components/schemas/ModelExtension"
          }
        }
      },
      "CatalogCapabilities": {
        "type": "object",
        "description": "What the catalog records this model can do. Each answer is one of three words, and `unknown` is not `no`: it means nobody has established the answer.",
        "additionalProperties": false,
        "required": [
          "audio",
          "json_mode",
          "streaming",
          "tools",
          "vision"
        ],
        "properties": {
          "audio": {
            "type": "string",
            "enum": [
              "no",
              "unknown",
              "yes"
            ]
          },
          "json_mode": {
            "type": "string",
            "enum": [
              "no",
              "unknown",
              "yes"
            ]
          },
          "streaming": {
            "type": "string",
            "enum": [
              "no",
              "unknown",
              "yes"
            ]
          },
          "tools": {
            "type": "string",
            "enum": [
              "no",
              "unknown",
              "yes"
            ]
          },
          "vision": {
            "type": "string",
            "enum": [
              "no",
              "unknown",
              "yes"
            ]
          }
        }
      },
      "ModelExtension": {
        "type": "object",
        "description": "Our own facts about a model, under the `hopscotch` key. Everything OpenAI's own model object defines stays at the top level; everything it does not define lives here.",
        "additionalProperties": true,
        "required": [
          "model_id",
          "display_name",
          "author",
          "input_rate_thousandths",
          "endpoint_families",
          "provider_display_names",
          "capabilities"
        ],
        "properties": {
          "model_id": {
            "type": "string",
            "description": "The same value as `id`, repeated inside the extension so a client reading only this block still has it."
          },
          "display_name": {
            "type": "string"
          },
          "author": {
            "type": "string",
            "description": "Who made the model, which is not who serves it."
          },
          "context_window_tokens": {
            "type": "integer",
            "nullable": true
          },
          "max_output_tokens": {
            "type": "integer",
            "nullable": true
          },
          "input_rate_thousandths": {
            "type": "integer",
            "description": "The provider's own list price for prompt tokens, in thousandths of the currency unit per million tokens. Nothing this platform charges on top is folded in: the fee is taken when credit is bought, and a request then costs the provider's price."
          },
          "output_rate_thousandths": {
            "type": "integer",
            "description": "The same unit, for completion tokens. Absent when the model publishes no separate output rate."
          },
          "currency_code": {
            "type": "string",
            "nullable": true,
            "description": "ISO 4217, or null where nobody recorded one."
          },
          "endpoint_families": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The request families this model answers."
          },
          "provider_display_names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Who can serve it. Which one answered a given request is on the response headers, not here."
          },
          "capabilities": {
            "$ref": "#/components/schemas/CatalogCapabilities"
          }
        }
      },
      "ModelParameter": {
        "type": "object",
        "description": "One request parameter this model accepts. The three bounds are in thousandths of the parameter's own unit, so a temperature ceiling of 2 arrives as 2000 and a default of 0.7 as 700. Integers, because a bound that drifted in its last decimal place would be a contract about nothing.",
        "additionalProperties": false,
        "required": [
          "name",
          "type",
          "min_milli",
          "max_milli",
          "default_milli",
          "enum_values",
          "sent_by_default",
          "note"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "boolean",
              "enum",
              "integer",
              "number",
              "string"
            ],
            "description": "The control shape a client should render."
          },
          "min_milli": {
            "type": "integer",
            "nullable": true
          },
          "max_milli": {
            "type": "integer",
            "nullable": true
          },
          "default_milli": {
            "type": "integer",
            "nullable": true
          },
          "enum_values": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "sent_by_default": {
            "type": "boolean",
            "description": "Whether a client sends this parameter unless it is told otherwise."
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ModelDetailExtension": {
        "description": "Everything the list carries for a model, plus the five fields served only when one model is asked for by name.",
        "allOf": [
          {
            "$ref": "#/components/schemas/ModelExtension"
          },
          {
            "type": "object",
            "additionalProperties": true,
            "required": [
              "description",
              "parameters",
              "deprecated_at",
              "sunset_at",
              "replacement_model_id"
            ],
            "properties": {
              "description": {
                "type": "string",
                "nullable": true,
                "description": "Detail only. The list is scanned and the detail is read, so the prose a model carries is served where somebody asked for that one model."
              },
              "parameters": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ModelParameter"
                }
              },
              "deprecated_at": {
                "type": "integer",
                "nullable": true,
                "description": "Unix seconds."
              },
              "sunset_at": {
                "type": "integer",
                "nullable": true,
                "description": "Unix seconds."
              },
              "replacement_model_id": {
                "type": "string",
                "nullable": true,
                "description": "What to move to, where one is named."
              }
            }
          }
        ]
      },
      "ModelDetail": {
        "type": "object",
        "description": "One model, in the same envelope the list uses, with the detail-only fields under `hopscotch`.",
        "additionalProperties": true,
        "properties": {
          "id": {
            "type": "string",
            "example": "openai/gpt-4o-mini"
          },
          "object": {
            "type": "string",
            "example": "model"
          },
          "created": {
            "type": "integer"
          },
          "owned_by": {
            "type": "string"
          },
          "shutdown_date": {
            "type": "integer",
            "description": "Absent when the model has no announced end."
          },
          "hopscotch": {
            "$ref": "#/components/schemas/ModelDetailExtension"
          }
        }
      }
    },
    "headers": {
      "RequestId": {
        "description": "Our id for this request. Quote it when you contact support. Present on every response, errors included.",
        "schema": {
          "type": "string"
        }
      },
      "SdkRequestId": {
        "description": "The same value as x-hopscotch-request-id, on the header the official OpenAI SDKs surface on their error objects.",
        "schema": {
          "type": "string"
        }
      },
      "Provider": {
        "description": "Which provider answered.",
        "schema": {
          "type": "string"
        }
      },
      "RetryAttemptCount": {
        "description": "How many attempts were made before the response you received.",
        "schema": {
          "type": "string"
        }
      },
      "CacheStatus": {
        "description": "Whether the reply came from cache.",
        "schema": {
          "type": "string"
        }
      },
      "TraceId": {
        "description": "A trace id for this request.",
        "schema": {
          "type": "string"
        }
      }
    },
    "securitySchemes": {
      "customerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A workspace API key in an `Authorization: Bearer` header. A missing header, a scheme other than Bearer, a key of the wrong shape, and a key we reject are all 401."
      }
    }
  },
  "security": [
    {
      "customerApiKey": []
    }
  ]
}
