{"openapi":"3.1.0","info":{"title":"Keera Gateway","summary":"The one endpoint every AI request passes through.","description":"One listener serves three APIs, told apart by path:\n\n- `/api` is the inference API. It speaks the OpenAI and Anthropic formats,\n  so their SDKs work with only a new base URL and an API key.\n- `/control` is the control API, which the panel and the `keera` command use.\n- `/sandbox` attaches to a sandbox.\n\nErrors share one shape: `{\"error\": {\"message\", \"type\", \"code\"}}`.\n","license":{"name":"Keera Community Licence v1.0","url":"https://github.com/bespinian/keera-gateway/blob/main/LICENSE"},"version":"devel"},"servers":[{"url":"/"}],"security":[{"bearer":[]},{"session":[]}],"tags":[{"name":"inference","description":"OpenAI- and Anthropic-shaped requests, behind an API key."},{"name":"mcp","description":"MCP servers, behind the same API keys."},{"name":"probes","description":"Liveness, readiness and metrics, for orchestrators and scrapers."},{"name":"sign-in","description":"Signing in and out of the control API."},{"name":"me","description":"The caller and their own access."},{"name":"tenancy","description":"Organisations, projects, users and passkeys."},{"name":"keys","description":"API keys."},{"name":"guardrails","description":"Limits and rules per organisation, project or key."},{"name":"filters"},{"name":"routers"},{"name":"models"},{"name":"mcp-servers"},{"name":"connect","description":"Setting up clients against this gateway."},{"name":"reports","description":"Usage, requests, sessions, audit and diagnostics."},{"name":"billing"},{"name":"sandboxes"},{"name":"spec","description":"This description, as YAML or JSON."}],"paths":{"/openapi.yaml":{"get":{"operationId":"getSpecYAML","tags":["spec"],"summary":"This spec, as YAML","security":[],"responses":{"200":{"description":"The spec, with `info.version` set to the running build.","content":{"application/yaml":{"schema":{"type":"string"}}}}}}},"/openapi.json":{"get":{"operationId":"getSpecJSON","tags":["spec"],"summary":"This spec, as JSON","security":[],"responses":{"200":{"description":"The spec, with `info.version` set to the running build.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/control/auth/config":{"get":{"operationId":"authConfig","tags":["sign-in"],"summary":"How to sign in","description":"What the panel offers on its sign-in screen, before anybody has signed in.","security":[],"responses":{"200":{"description":"The ways in.","content":{"application/json":{"schema":{"type":"object","required":["sso","providers","passkeys"],"properties":{"sso":{"type":"boolean","description":"Whether an identity provider is configured."},"providers":{"type":"array","items":{"$ref":"#/components/schemas/SignInProvider"}},"passkeys":{"type":"boolean","description":"Whether passkeys are on."}}}}}}}}},"/control/auth/login":{"get":{"operationId":"login","tags":["sign-in"],"summary":"Start a sign-in","description":"Opened in a browser. It sends the browser to the identity provider,\nor for a password directory or passkeys to the panel's own form.\n`keera login` and Keera Chat add the `cli_*` parameters, so the\nsign-in ends at their address with a one-time code for\n`POST /control/auth/cli/token`.\n\nA sign-in that cannot start also redirects, to `/?sign_in_error=1`,\nwith the reason in the `keera_sign_in_error` cookie.\n","security":[],"parameters":[{"name":"provider","in":"query","description":"The identity provider's name, or `passkey`. It may be left out\nonly when there is one way in.\n","schema":{"type":"string"}},{"name":"next","in":"query","description":"A path on this site to go to afterwards. Anything else is ignored.","schema":{"type":"string"}},{"name":"cli_redirect","in":"query","description":"Where a command-line sign-in ends: a loopback address such as\n`http://127.0.0.1:1234/callback`, or Keera Chat's callback under\n`KEERA_CHAT_URL`.\n","schema":{"type":"string","format":"uri"}},{"name":"cli_challenge","in":"query","description":"The PKCE S256 challenge, 32 bytes as unpadded base64url. Needed with `cli_redirect`.","schema":{"type":"string"}},{"name":"cli_state","in":"query","description":"Handed back with the code. Needed with `cli_redirect`; up to 128 characters.","schema":{"type":"string","maxLength":128}}],"responses":{"302":{"description":"To the identity provider, to the panel's password or passkey\nform, or back to the sign-in screen with an error.\n","headers":{"Location":{"schema":{"type":"string"}},"Set-Cookie":{"description":"`keera_login`, the state the callback must match.","schema":{"type":"string"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/auth/callback":{"get":{"operationId":"callback","tags":["sign-in"],"summary":"Finish a sign-in","description":"Where the identity provider sends the browser back. It only works in\nthe browser that started the sign-in.\n","security":[],"parameters":[{"name":"code","in":"query","schema":{"type":"string"}},{"name":"state","in":"query","schema":{"type":"string"}},{"name":"error","in":"query","description":"Set by the identity provider when it refused.","schema":{"type":"string"}},{"name":"error_description","in":"query","schema":{"type":"string"}},{"name":"keera_login","in":"cookie","description":"Set by `GET /control/auth/login`; must match `state`.","schema":{"type":"string"}}],"responses":{"302":{"description":"Signed in, to `next` or `/`; for a command-line sign-in, to its\naddress with `code` and `state`. Somebody new where sign-up is\nallowed goes to `/?sign_up=1`. A failed sign-in goes to\n`/?sign_in_error=1`, with the reason in the `keera_sign_in_error`\ncookie.\n","headers":{"Location":{"schema":{"type":"string"}},"Set-Cookie":{"description":"`keera_session` once signed in, or `keera_signup` for the sign-up screen.","schema":{"type":"string"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/control/auth/local":{"post":{"operationId":"localLogin","tags":["sign-in"],"summary":"Sign in with the operator key","description":"Starts a panel session that grants what the operator key grants.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["key"],"properties":{"key":{"type":"string"}}}}}},"responses":{"200":{"description":"Signed in.","headers":{"Set-Cookie":{"description":"`keera_session`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","const":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/auth/cli/token":{"post":{"operationId":"cliToken","tags":["sign-in"],"summary":"Redeem a sign-in code for a CLI token","description":"The last step of `keera login`. The code works once, and only with\nthe PKCE verifier of the process that started the sign-in. Send the\ntoken as `Authorization: Bearer \u003ctoken\u003e`; `GET /control/v1/me` says\nwhose it is.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["code","verifier"],"properties":{"code":{"type":"string"},"verifier":{"type":"string"},"label":{"type":"string","description":"How the machine describes itself, for the audit log."}}}}}},"responses":{"200":{"description":"The token.","content":{"application/json":{"schema":{"type":"object","required":["token","expires_at"],"properties":{"token":{"type":"string","description":"Starts with `keera_cli_`. Shown only here."},"expires_at":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/auth/password":{"post":{"operationId":"passwordSignIn","tags":["sign-in"],"summary":"Sign in with a directory password","description":"For an LDAP directory. Attempts are also limited per account, from\nany address. Must be sent as `application/json`.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["provider","username","password"],"properties":{"provider":{"type":"string","description":"A provider whose `password` is true."},"flow":{"type":"string","description":"The sign-in `GET /control/auth/login` started for\n`keera login`, from the panel's address. Leave it out to\nsign in to the panel.\n"},"username":{"type":"string"},"password":{"type":"string","format":"password"},"next":{"type":"string","description":"A path on this site to go to afterwards."}}}}}},"responses":{"200":{"description":"Signed in.","headers":{"Set-Cookie":{"description":"`keera_session`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInRedirect"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"415":{"description":"The body was not sent as `application/json`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"description":"The directory did not answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/auth/passkey/options":{"post":{"operationId":"passkeySignInOptions","tags":["sign-in"],"summary":"Start a passkey sign-in","description":"Starts a new sign-in, or with `flow` continues the one\n`GET /control/auth/login` started for `keera login`. Must be sent as\n`application/json`.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"flow":{"type":"string"},"next":{"type":"string","description":"A path on this site to go to afterwards."}}}}}},"responses":{"200":{"description":"What to call `navigator.credentials.get` with.","content":{"application/json":{"schema":{"type":"object","required":["flow","options"],"properties":{"flow":{"type":"string","description":"Send it back with the answer."},"options":{"$ref":"#/components/schemas/RequestOptions"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"415":{"description":"The body was not sent as `application/json`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"501":{"$ref":"#/components/responses/Disabled"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/auth/passkey/sign-in":{"post":{"operationId":"passkeySignIn","tags":["sign-in"],"summary":"Sign in with a passkey","description":"Each `flow` works once. Must be sent as `application/json`.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["flow","credential"],"properties":{"flow":{"type":"string"},"credential":{"$ref":"#/components/schemas/Assertion"}}}}}},"responses":{"200":{"description":"Signed in.","headers":{"Set-Cookie":{"description":"`keera_session`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInRedirect"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"415":{"description":"The body was not sent as `application/json`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"501":{"$ref":"#/components/responses/Disabled"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/auth/passkey/setup/options":{"post":{"operationId":"passkeySetupOptions","tags":["sign-in"],"summary":"Start adding a passkey from a set-up link","description":"Nobody is signed in: the link's token is the credential. Using the\noptions does not use up the link. Must be sent as `application/json`.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["token"],"properties":{"token":{"type":"string","description":"The part of the set-up link after `#passkey-setup=`."}}}}}},"responses":{"200":{"description":"What to call `navigator.credentials.create` with.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistrationOptions"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"415":{"description":"The body was not sent as `application/json`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"501":{"$ref":"#/components/responses/Disabled"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/auth/passkey/setup":{"post":{"operationId":"passkeySetup","tags":["sign-in"],"summary":"Add a passkey from a set-up link","description":"Stores the passkey, uses up the link and signs the person in. Must be\nsent as `application/json`.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["token","challenge_id","credential"],"properties":{"token":{"type":"string","description":"The same token as for the options."},"challenge_id":{"type":"string"},"name":{"type":"string","description":"Defaults to \"Passkey\". Cut to 64 characters."},"credential":{"$ref":"#/components/schemas/Registration"}}}}}},"responses":{"200":{"description":"Signed in.","headers":{"Set-Cookie":{"description":"`keera_session`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInRedirect"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"415":{"description":"The body was not sent as `application/json`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"501":{"$ref":"#/components/responses/Disabled"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/auth/signup":{"get":{"operationId":"signupInfo","tags":["sign-in"],"summary":"Read the sign-up in progress","description":"For a sign-in that matched no organisation, through a provider that\nallows sign-up. Says who signed in and who invited them.\n","security":[],"parameters":[{"name":"keera_signup","in":"cookie","required":true,"description":"Set by the callback.","schema":{"type":"string"}}],"responses":{"200":{"description":"The sign-up.","content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string"},"invited_org_name":{"type":"string","description":"The organisation that added this address, if any."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"completeSignup","tags":["sign-in"],"summary":"Create or join an organisation","description":"Creates a limited organisation with the person as its administrator,\nor joins the one that invited them, and signs them in. A name another\norganisation has gets the person's address added.\n","security":[],"parameters":[{"name":"keera_signup","in":"cookie","required":true,"description":"Set by the callback.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"join":{"type":"boolean","description":"Join the inviting organisation instead of creating one."},"name":{"type":"string","minLength":1,"maxLength":100,"description":"The new organisation's name. Needed unless `join` is true."}}}}}},"responses":{"200":{"description":"Signed in.","headers":{"Set-Cookie":{"description":"`keera_session`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInRedirect"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/auth/logout":{"post":{"operationId":"logout","tags":["sign-in"],"summary":"Sign out","description":"Ends this session or CLI token. With `all=1`, ends every session and\nCLI token of this person, in every organisation.\n","parameters":[{"name":"all","in":"query","description":"`1` to sign out everywhere.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Signed out.","headers":{"Set-Cookie":{"description":"Clears `keera_session`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["provider_logout_url"],"properties":{"provider_logout_url":{"type":"string","description":"The identity provider's own sign-out page, or empty."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/control/v1/me":{"get":{"operationId":"me","tags":["me"],"summary":"Who the caller is","description":"What the panel loads first. A session gets its CSRF token here.","responses":{"200":{"description":"The caller and what this deployment offers.","content":{"application/json":{"schema":{"type":"object","required":["via","user_id","email","name","role","org_id","unrestricted","can_admin_org","csrf","sso","providers","roles_from_directory","gateway_url","chat_url","sandboxes","billing","payments","claude_subscriptions","passkeys","passkey_account"],"properties":{"via":{"type":"string","enum":["operator_key","session","cli"]},"user_id":{"type":"string","description":"Empty for the operator key."},"email":{"type":"string"},"name":{"type":"string"},"role":{"type":"string","enum":["operator","admin","member"]},"org_id":{"type":"string","description":"Empty for the operator key."},"unrestricted":{"type":"boolean","description":"Whether the caller is an operator."},"can_admin_org":{"type":"boolean"},"csrf":{"type":"string","description":"For `X-CSRF-Token`. Empty unless signed in with a session."},"sso":{"type":"boolean"},"providers":{"type":"array","items":{"$ref":"#/components/schemas/SignInProvider"}},"roles_from_directory":{"type":"boolean","description":"Whether a directory group decides roles, so they cannot be changed here."},"gateway_url":{"type":"string","description":"Where clients send inference requests."},"chat_url":{"type":"string","description":"Keera Chat, or empty."},"sandboxes":{"type":"boolean"},"billing":{"type":"boolean"},"payments":{"type":"boolean"},"claude_subscriptions":{"type":"boolean"},"passkeys":{"type":"boolean"},"passkey_account":{"type":"boolean","description":"Whether the caller signs in with passkeys."},"orgs":{"type":"array","description":"The organisations to switch to, by name. Only when there is more than one.","items":{"$ref":"#/components/schemas/Membership"}},"org_name":{"type":"string"},"org_limited":{"type":"boolean","description":"Whether the organisation is limited, as after sign-up."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/control/v1/me/org":{"post":{"operationId":"switchOrg","tags":["me"],"summary":"Switch organisation","description":"Moves the caller's session or CLI token to another of their\norganisations. Choosing one that invited them accepts the invitation.\nThe operator key cannot switch; it names `org_id` on each request.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["org_id"],"properties":{"org_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Switched.","content":{"application/json":{"schema":{"type":"object","required":["org_id","org_name","role"],"properties":{"org_id":{"type":"string"},"org_name":{"type":"string"},"role":{"type":"string","enum":["operator","admin","member"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/v1/access":{"get":{"operationId":"access","tags":["me"],"summary":"My keys, limits and refusals","description":"Only the caller's own keys, even for an administrator, with the\nlimits above each and the refusals of the last week.\n","responses":{"200":{"description":"The caller's access.","content":{"application/json":{"schema":{"type":"object","required":["email","role","gateway_url","keys","refusals"],"properties":{"email":{"type":"string"},"role":{"type":"string","enum":["operator","admin","member"]},"gateway_url":{"type":"string"},"keys":{"type":"array","items":{"$ref":"#/components/schemas/AccessKey"}},"refusals":{"type":"array","description":"The last 20 refused or failed requests, newest first.","items":{"$ref":"#/components/schemas/Refusal"}},"anonymous":{"type":"boolean","description":"True for the operator key, which has no keys."},"operator_key":{"type":"boolean","description":"True when signed in to the panel with the operator key."},"org":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"used":{"type":"boolean","description":"Whether any of the keys has ever been used."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/control/v1/users":{"post":{"operationId":"addUser","tags":["tenancy"],"summary":"Add a person","description":"Places a person in an organisation before their first sign-in.\nNeeds an administrator of the organisation. A passkey account comes\nwith its first set-up link; an administrator can only create one at\nthe organisation's email domain.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["email"],"properties":{"org_id":{"type":"string","description":"Only an operator, who belongs to none, needs it."},"email":{"type":"string","format":"email"},"external_id":{"type":"string","description":"`\u003cprovider\u003e:\u003csubject\u003e`, such as `google:1234567890`. Only\nan operator may set it; otherwise the person is linked at\ntheir first sign-in.\n"},"role":{"type":"string","enum":["admin","member"],"default":"member"},"sign_in":{"type":"string","enum":["sso","passkey"],"description":"Defaults to `sso`, or `passkey` on a gateway with passkeys\nand no identity provider.\n"}}}}}},"responses":{"201":{"description":"Added.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/User"},{"type":"object","properties":{"passkey_link":{"$ref":"#/components/schemas/PasskeyLink","description":"Only for a passkey account."}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"501":{"$ref":"#/components/responses/Disabled"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"get":{"operationId":"listUsers","tags":["tenancy"],"summary":"List people","description":"An operator who names no organisation sees everyone, by address.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"The people.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/users/{id}":{"patch":{"operationId":"updateUser","tags":["tenancy"],"summary":"Change a person's role","description":"Needs an administrator of the person's organisation. Not allowed\nwhere a directory group decides roles, for an operator, or on\nyourself. Signs the person out, so the new role applies at once.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["role"],"properties":{"role":{"type":"string","enum":["admin","member"]}}}}}},"responses":{"200":{"description":"Changed.","content":{"application/json":{"schema":{"type":"object","required":["id","role"],"properties":{"id":{"type":"string"},"role":{"type":"string","enum":["admin","member"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/v1/users/{id}/disable":{"post":{"operationId":"disableUser","tags":["tenancy"],"summary":"Turn a person off","description":"For someone who left. Revokes their keys, ends their sessions,\nremoves their passkeys and stops their sandboxes. Needs an\nadministrator of their organisation; not for yourself or an operator.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Turned off.","content":{"application/json":{"schema":{"type":"object","required":["id","email","disabled","revoked_keys"],"properties":{"id":{"type":"string"},"email":{"type":"string"},"disabled":{"type":"boolean","const":true},"revoked_keys":{"type":"integer"},"sandboxes":{"type":"object","description":"Only where this deployment lends out sandboxes.","required":["terminated","suspended","failed"],"properties":{"terminated":{"type":"integer"},"suspended":{"type":"integer","description":"An engineer's sandbox is kept, suspended."},"failed":{"type":"integer"}}},"warning":{"type":"string","description":"Set when some sandboxes could not be read or stopped."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/users/{id}/enable":{"post":{"operationId":"enableUser","tags":["tenancy"],"summary":"Turn a person back on","description":"They can sign in again. Their old keys stay revoked. Needs an\nadministrator of their organisation; not for yourself or an operator.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Turned on.","content":{"application/json":{"schema":{"type":"object","required":["id","email","disabled"],"properties":{"id":{"type":"string"},"email":{"type":"string"},"disabled":{"type":"boolean","const":false}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/users/{id}/passkey-link":{"post":{"operationId":"issuePasskeyLinkRoute","tags":["tenancy"],"summary":"Hand out a passkey set-up link","description":"Replaces any earlier link. Turns a person who has not signed in yet\ninto a passkey account. Needs an administrator of their organisation,\nand, for one, an address at the organisation's email domain.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"The link. It is shown only here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasskeyLink"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/passkeys":{"get":{"operationId":"listPasskeys","tags":["tenancy"],"summary":"List passkeys","description":"The caller's own, or with `user_id` someone the caller administers.","parameters":[{"name":"user_id","in":"query","description":"Needed with the operator key, which has no passkeys.","schema":{"type":"string"}}],"responses":{"200":{"description":"The passkeys.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Passkey"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"501":{"$ref":"#/components/responses/Disabled"}}},"post":{"operationId":"addOwnPasskey","tags":["tenancy"],"summary":"Add a passkey to your account","description":"Finishes what `POST /control/v1/passkeys/options` started. Only for a\npasskey account, in the panel, within 15 minutes of signing in.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["challenge_id","credential"],"properties":{"challenge_id":{"type":"string"},"name":{"type":"string","description":"Defaults to \"Passkey\". Cut to 64 characters."},"credential":{"$ref":"#/components/schemas/Registration"}}}}}},"responses":{"201":{"description":"Added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Passkey"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"501":{"$ref":"#/components/responses/Disabled"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/v1/passkeys/options":{"post":{"operationId":"ownPasskeyOptions","tags":["tenancy"],"summary":"Start adding a passkey to your account","description":"For a second device. Only for a passkey account, in the panel, within\n15 minutes of signing in; otherwise the code is `sign_in_again` or\n`forbidden`. Takes no body.\n","responses":{"200":{"description":"What to call `navigator.credentials.create` with.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistrationOptions"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/passkeys/{id}":{"delete":{"operationId":"deletePasskey","tags":["tenancy"],"summary":"Remove a passkey","description":"Anyone may remove their own, but not their last. An administrator may\nremove anyone's in their organisation, which also signs that person\nout.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","required":["id","deleted"],"properties":{"id":{"type":"string"},"deleted":{"type":"boolean","const":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/orgs":{"post":{"operationId":"createOrg","tags":["tenancy"],"summary":"Create an organisation","description":"Only an operator may. The new organisation starts with the models and\nsandbox classes of the template files, and a project called `default`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1},"email_domain":{"type":"string","description":"Places a first sign-in from this domain in this organisation.","examples":["example.com"]}}}}}},"responses":{"201":{"description":"The new organisation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Org"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict","description":"The name or email domain belongs to another organisation (`org_name_taken`, `domain_taken`)."},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"get":{"operationId":"listOrgs","tags":["tenancy"],"summary":"List organisations","description":"An operator sees every organisation. Anyone else sees only their own.","responses":{"200":{"description":"The organisations.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Org"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/control/v1/orgs/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"updateOrg","tags":["tenancy"],"summary":"Change an organisation","description":"Only an operator may. Send at least one field.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"name":{"type":"string","minLength":1},"email_domain":{"type":"string","description":"An empty string clears it."},"limited":{"type":"boolean","description":"false lifts the limit of an organisation that signed up."}}}}}},"responses":{"200":{"description":"The organisation as it is now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Org"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"The name or email domain belongs to another organisation (`org_name_taken`, `domain_taken`)."},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"delete":{"operationId":"deleteOrg","tags":["tenancy"],"summary":"Delete an organisation","description":"Only an operator may, and not on the organisation they are signed in\nto. It removes the organisation's projects, users and keys. The request\nlog, billing and audit log stay.\n","responses":{"200":{"description":"What the delete removed.","content":{"application/json":{"schema":{"type":"object","required":["id","name","deleted","projects","users","keys"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"deleted":{"type":"boolean","const":true},"projects":{"type":"integer","description":"How many projects were removed."},"users":{"type":"integer","description":"How many users were removed."},"keys":{"type":"integer","description":"How many keys were removed."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"It still has live sandboxes (`org_has_sandboxes`). Terminate them first."}}}},"/control/v1/projects":{"post":{"operationId":"createProject","tags":["tenancy"],"summary":"Create a project","description":"Only an administrator of the organisation may.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"org_id":{"type":"string","description":"The organisation. Only an operator needs it; everyone else\ncreates in their own.\n"},"name":{"type":"string","minLength":1},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"The new project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"Another project in the organisation has that name (`project_name_taken`)."},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"get":{"operationId":"listProjects","tags":["tenancy"],"summary":"List projects","description":"Each project comes with its guardrails and its spend this period. An\noperator who sends no `org_id` gets every organisation's projects.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"The projects, by name.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ProjectSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/projects/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"updateProject","tags":["tenancy"],"summary":"Rename or describe a project","description":"Only an administrator of the organisation may. Send at least one field.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"name":{"type":"string","minLength":1},"description":{"type":"string","description":"An empty string clears it."}}}}}},"responses":{"200":{"description":"The project as it is now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"Another project in the organisation has that name (`project_name_taken`)."},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"delete":{"operationId":"deleteProject","tags":["tenancy"],"summary":"Delete a project","description":"Only an administrator of the organisation may. Every key of the\nproject must be revoked first. Revoked keys stay, with no project.\n","responses":{"200":{"description":"The project is gone.","content":{"application/json":{"schema":{"type":"object","required":["id","name","deleted","detached_keys"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"deleted":{"type":"boolean","const":true},"detached_keys":{"type":"integer","description":"How many revoked keys stayed behind, now in no project."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"It still has keys that are not revoked (`project_has_keys`), or\nlive sandboxes (`project_has_sandboxes`).\n"}}}},"/control/v1/keys":{"post":{"operationId":"createKey","tags":["keys"],"summary":"Issue an API key","description":"Only an administrator of the organisation may. The secret is in `key`\nand is shown only this once. A pending key has no secret yet: the\nperson it is for gets one when they activate it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"org_id":{"type":"string","description":"The organisation. Only an operator needs it."},"project_id":{"type":"string","description":"Leave it out for the organisation's oldest project."},"user_id":{"type":"string","description":"The person whose spend this is. Required for a\nsubscription key and for a pending key.\n"},"name":{"type":"string","description":"Defaults to `unnamed`."},"kind":{"$ref":"#/components/schemas/KeyKind"},"expires_in_seconds":{"type":"integer","format":"int64","minimum":1,"description":"Lifetime from now. Not with `expires_at`. Neither means it never expires."},"expires_at":{"type":"string","format":"date-time","description":"When it expires; must be in the future. Not with `expires_in_seconds`."},"pending":{"type":"boolean","description":"Leave the key for the person named in `user_id` to activate."}}}}}},"responses":{"201":{"description":"The new key.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/KeyInfo"},{"type":"object","properties":{"key":{"type":"string","description":"The secret. Shown only now. Absent for a pending key."}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The person or project is not in this organisation."},"409":{"$ref":"#/components/responses/Conflict","description":"The organisation has no projects yet (`project_required`)."},"501":{"$ref":"#/components/responses/Disabled","description":"A subscription key, while this deployment takes no Claude subscriptions (`subscriptions_disabled`)."},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"get":{"operationId":"listKeys","tags":["keys"],"summary":"List API keys","description":"Each key comes with its own guardrails, last use and spend since `since`.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"project_id","in":"query","description":"Only this project's keys.","schema":{"type":"string"}},{"name":"since","in":"query","description":"How far back to count spend, as a duration such as `24h`. The\ndefault is the start of this month (UTC). It is rounded down to\nmidnight.\n","schema":{"type":"string","examples":["168h"]}}],"responses":{"200":{"description":"The keys.","content":{"application/json":{"schema":{"type":"object","required":["data","since"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/KeySummary"}},"since":{"type":"string","format":"date-time","description":"Where the counts start."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/keys/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"renameKey","tags":["keys"],"summary":"Rename an API key","description":"An administrator may, and so may a member for a key in their own name.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1}}}}}},"responses":{"200":{"description":"The new name.","content":{"application/json":{"schema":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"delete":{"operationId":"revokeKey","tags":["keys"],"summary":"Revoke an API key","description":"An administrator may, and so may a member for a key in their own name.\nThe key stays listed, revoked, so its usage still has a name.\n","responses":{"200":{"description":"The key no longer works.","content":{"application/json":{"schema":{"type":"object","required":["id","revoked"],"properties":{"id":{"type":"string"},"revoked":{"type":"boolean","const":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"It was already revoked (`key_revoked`)."}}}},"/control/v1/keys/{id}/rotate":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"rotateKey","tags":["keys"],"summary":"Rotate an API key","description":"Issues a new key with the old one's project, person, kind and\nguardrails, and revokes the old one. An administrator may, and so may a\nmember for a key in their own name, but only an administrator may set\nits lifetime. The secret is in `key` and is shown only this once.\n","requestBody":{"required":true,"description":"Send `{}` to keep the name and lifetime.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","description":"Empty keeps the old name."},"expires_in_seconds":{"type":"integer","format":"int64","minimum":1,"description":"Lifetime from now. Not with `expires_at`. Neither gives the\nnew key the old one's lifetime, counted from now.\n"},"expires_at":{"type":"string","format":"date-time","description":"When it expires; must be in the future."}}}}}},"responses":{"201":{"description":"The new key.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/KeyInfo"},{"type":"object","required":["key","replaced_key_id"],"properties":{"key":{"type":"string","description":"The secret. Shown only now."},"replaced_key_id":{"type":"string","description":"The old key, now revoked."}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"It is revoked (`key_revoked`), not yet activated (`key_pending`),\nor belongs to a sandbox (`sandbox_key`).\n"},"501":{"$ref":"#/components/responses/Disabled","description":"A subscription key, while this deployment takes no Claude subscriptions (`subscriptions_disabled`)."},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/v1/keys/{id}/activate":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"operationId":"activateKey","tags":["keys"],"summary":"Activate a pending API key","description":"Gives a pending key its secret. The person it is for may, and so may an\nadministrator. The secret is in `key` and is shown only this once.\n","requestBody":{"required":true,"description":"Send `{}` to keep the name.","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","description":"A new name, such as the machine's. Empty keeps it."}}}}}},"responses":{"200":{"description":"The key, now active.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/KeyInfo"},{"type":"object","required":["key"],"properties":{"key":{"type":"string","description":"The secret. Shown only now."}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"It is revoked (`key_revoked`), expired (`key_expired`) or already\nactive (`key_active`).\n"},"501":{"$ref":"#/components/responses/Disabled","description":"A subscription key, while this deployment takes no Claude subscriptions (`subscriptions_disabled`)."},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/v1/guardrails/{scope}/{id}":{"parameters":[{"name":"scope","in":"path","required":true,"description":"The level the guardrail is on.","schema":{"$ref":"#/components/schemas/ScopeType"}},{"name":"id","in":"path","required":true,"description":"The id of the organisation, project or key.","schema":{"type":"string"}}],"get":{"operationId":"getGuardrails","tags":["guardrails"],"summary":"Read a guardrail","description":"What this one scope sets on its own. A scope with no guardrail answers\nan empty one. Anyone in the organisation may read it.\n","responses":{"200":{"description":"The scope's own guardrail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Limits"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"operationId":"putGuardrails","tags":["guardrails"],"summary":"Set a guardrail","description":"Replaces the scope's guardrail with the body. Only an administrator of\nthe organisation may. On an organisation, only an operator may change\n`allowed_repos`; an administrator who leaves it out keeps it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Limits"}}}},"responses":{"200":{"description":"The guardrail as stored, cleaned up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Limits"}}}},"400":{"$ref":"#/components/responses/BadRequest","description":"The guardrail is not valid, or names a filter or MCP server the\norganisation does not have (`filter_not_found`,\n`mcp_server_not_found`).\n"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict","description":"An organisation's allow-list leaves out a model that one of its\nfilters or routers runs on (`filter_model_not_allowed`,\n`router_model_not_allowed`).\n"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/v1/guardrails/{scope}/{id}/effective":{"parameters":[{"name":"scope","in":"path","required":true,"description":"The level the guardrail is on.","schema":{"$ref":"#/components/schemas/ScopeType"}},{"name":"id","in":"path","required":true,"description":"The id of the organisation, project or key.","schema":{"type":"string"}}],"get":{"operationId":"effectiveGuardrails","tags":["guardrails"],"summary":"Read the guardrails that apply","description":"Every level above the scope, and what they combine to, as the gateway\ncombines them. It answers \"why is this key refused\". Anyone in the\norganisation may read it.\n","responses":{"200":{"description":"The levels and their result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Effective"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/filters":{"get":{"operationId":"listFilters","tags":["filters"],"summary":"List filters","description":"Every member may read it, instructions included. With `stats=1` it also\nsays what each filter did in the window.\n","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"stats","in":"query","description":"\"1\" adds `stats`, `from` and `to`.","schema":{"type":"string","enum":["1"]}},{"name":"from","in":"query","description":"Start of the stats window. Defaults to 30 days ago.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the stats window. Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The organisation's filters.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Filter"}},"stats":{"type":"object","description":"By alias. A filter that did not run is left out.","additionalProperties":{"$ref":"#/components/schemas/FilterStat"}},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/filters/{alias}":{"parameters":[{"name":"alias","in":"path","required":true,"description":"Lowercase letters, digits and inner hyphens, up to 64.","schema":{"type":"string"}}],"put":{"operationId":"putFilter","tags":["filters"],"summary":"Create or replace a filter","description":"Administrators only. A `rewrite` or `gate` filter needs `model` and\n`prompt`, and no `rules`. A `pattern` filter needs `rules`, and no\n`model` or `prompt`. See https://docs.keera.ch/manage/filters/.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"model":{"type":"string","description":"The model the filter runs on: a chat model, or a decision\nmodel for a gate. The organisation must allow it.\n"},"mode":{"type":"string","enum":["rewrite","gate","pattern"],"description":"Empty or left out means rewrite."},"shadow":{"type":"boolean","description":"Run, record, but enforce nothing. Left out means enforce."},"prompt":{"type":"string","maxLength":16384,"description":"The instruction for the filter's model, up to 16 KiB."},"rules":{"type":"array","maxItems":64,"items":{"$ref":"#/components/schemas/FilterRule"}},"description":{"type":"string"}}}}}},"responses":{"200":{"description":"The filter as saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Filter"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"delete":{"operationId":"deleteFilter","tags":["filters"],"summary":"Delete a filter","description":"Administrators only. A filter that a router step or a guardrail still\nuses is refused with 409.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AliasDeleted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/control/v1/filters/{alias}/check":{"post":{"operationId":"checkFilter","tags":["filters"],"summary":"Run a filter over a sample","description":"Administrators only. Sends a fixed sample through the filter and shows\nwhat came back. A filter that fails still answers 200, with `ok` false.\nIt runs the filter's model, so it can cost money.\n","parameters":[{"name":"alias","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"What the check found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FilterProbe"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/filters/{alias}/report":{"get":{"operationId":"filterReport","tags":["filters"],"summary":"One filter's report","description":"Every member may read it. A deleted filter keeps its report; then\n`filter` is left out.\n","parameters":[{"name":"alias","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgID"},{"name":"from","in":"query","description":"Defaults to 30 days ago.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The report.","content":{"application/json":{"schema":{"type":"object","required":["report","project_names"],"properties":{"report":{"$ref":"#/components/schemas/FilterReport"},"filter":{"$ref":"#/components/schemas/Filter"},"project_names":{"type":"object","description":"Project names by id.","additionalProperties":{"type":"string"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/routers":{"get":{"operationId":"listRouters","tags":["routers"],"summary":"List routers","description":"Every member may read it. With `stats=1` it also says what went\nthrough each router in the window.\n","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"stats","in":"query","description":"\"1\" adds `stats`, `from` and `to`.","schema":{"type":"string","enum":["1"]}},{"name":"from","in":"query","description":"Start of the stats window. Defaults to 30 days ago.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the stats window. Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The organisation's routers.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Router"}},"stats":{"type":"object","description":"By alias. A router nobody called is left out.","additionalProperties":{"$ref":"#/components/schemas/RouterStat"}},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/routers/{alias}":{"parameters":[{"name":"alias","in":"path","required":true,"description":"Lowercase letters, digits and inner hyphens, up to 64.","schema":{"type":"string"}}],"put":{"operationId":"putRouter","tags":["routers"],"summary":"Create or replace a router","description":"Administrators only. The alias may not be one of the organisation's\nmodels (409). A new router is put on the organisation's allow-list,\nif it has one, unless the list leaves out a model the router reaches;\n`warning` then says so. See https://docs.keera.ch/manage/routers/.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["start"],"properties":{"display_name":{"type":"string"},"description":{"type":"string"},"start":{"$ref":"#/components/schemas/RouterStep"}}}}}},"responses":{"200":{"description":"The router as saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedRouter"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"delete":{"operationId":"deleteRouter","tags":["routers"],"summary":"Delete a router","description":"Administrators only. It also comes off the organisation's allow-list.\nA router that a project's or key's allow-list still names is refused\nwith 409.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AliasDeleted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/control/v1/routers/{alias}/check":{"post":{"operationId":"checkRouter","tags":["routers"],"summary":"Check a router's route steps","description":"Administrators only. An instruction step is given sample prompts and\nshows where each went; the other modes ask their destinations whether\nthey answer. It can cost money. A router with no route step, or no\nstep named by `step`, answers 400.\n","parameters":[{"name":"alias","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgID"},{"name":"step","in":"query","description":"Check only the route step with this id.","schema":{"type":"string"}}],"responses":{"200":{"description":"One probe per route step checked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouterCheck"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/routers/{alias}/report":{"get":{"operationId":"routerReport","tags":["routers"],"summary":"One router's report","description":"Every member may read it. A deleted router keeps its report; then\n`router` is left out.\n","parameters":[{"name":"alias","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgID"},{"name":"from","in":"query","description":"Defaults to 30 days ago.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The report.","content":{"application/json":{"schema":{"type":"object","required":["report"],"properties":{"report":{"$ref":"#/components/schemas/RouterReport"},"router":{"$ref":"#/components/schemas/Router"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/models":{"get":{"operationId":"listModels","tags":["models"],"summary":"List models","description":"Anyone signed in may read it. Only the organisation's administrators\nsee `backends` (`null` for everyone else) and `has_api_key`. With\n`stats=1` it adds each model's time to first token in the window.\n","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"stats","in":"query","description":"\"1\" adds `stats`, `from` and `to`.","schema":{"type":"string","enum":["1"]}},{"name":"from","in":"query","description":"Start of the stats window. Defaults to 30 days ago.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the stats window. Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The organisation's models.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrgModel"}},"stats":{"type":"object","description":"By alias. A model with no answers is left out.","additionalProperties":{"$ref":"#/components/schemas/ModelStat"}},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/providers":{"get":{"operationId":"listProviders","tags":["models"],"summary":"List hosted providers","description":"The hosted providers this build knows, with their models and prices.\nAnyone signed in may read it; only an administrator sees `endpoint`\n(empty for everyone else).\n","responses":{"200":{"description":"The providers.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/HostedProvider"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/control/v1/models/{alias}":{"parameters":[{"name":"alias","in":"path","required":true,"description":"Lowercase letters, digits and inner hyphens, with optional slashes\nbetween parts, such as `anthropic/claude-sonnet-4-5`. Send a slash\nas `%2F`.\n","schema":{"type":"string"}}],"put":{"operationId":"putModel","tags":["models"],"summary":"Create or replace a model","description":"Administrators only. A new model is put on the organisation's\nallow-list, if it has one. For a provider whose key the deployment\nholds, the backend and prices are set by the gateway and `api_key`\nmust be left out. See https://docs.keera.ch/manage/models/.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["backend_model"],"properties":{"kind":{"type":"string","enum":["chat","completion","embedding","decision"],"description":"Defaults to chat."},"backends":{"type":"array","items":{"type":"string","format":"uri"},"description":"http or https base URLs, such as `http://vllm:8000/v1`. At\nleast one, unless the deployment holds the provider's key.\n"},"backend_model":{"type":"string","description":"The name the backend serves the model under."},"provider":{"type":"string","description":"A hosted provider from `GET /control/v1/providers`, or empty for a self-served model."},"display_name":{"type":"string"},"description":{"type":"string"},"input_micros_per_mtok":{"type":"integer","format":"int64","description":"Micro-units per million input tokens."},"output_micros_per_mtok":{"type":"integer","format":"int64","description":"Micro-units per million output tokens."},"cached_input_micros_per_mtok":{"type":"integer","format":"int64","description":"0 means the input price."},"cache_write_micros_per_mtok":{"type":"integer","format":"int64","description":"0 means the input price."},"long_prompt":{"$ref":"#/components/schemas/PriceTier"},"max_context_tokens":{"type":"integer"},"release_date":{"type":"string","format":"date","description":"YYYY-MM-DD."},"location":{"type":"string","description":"Where the model runs, such as `ch`, `usa` or `onprem`. Left\nout, it comes from the provider or the backend address.\n"},"subscription":{"type":"boolean","description":"Paid by each caller's own Claude plan. Needs provider\n`anthropic`, kind chat, and no `api_key`. See\nhttps://docs.keera.ch/operate/claude-plans/.\n"},"enabled":{"type":"boolean","description":"Left out, a new model starts on and an existing one keeps its state."},"api_key":{"type":"string","writeOnly":true,"description":"The backend's credential. Left out keeps the stored one,\nand \"\" removes it. Needed again when `backends` moves to\nanother host.\n"},"alias":{"type":"string","description":"Ignored; the path names the model."},"org_id":{"type":"string","description":"Ignored."},"has_api_key":{"type":"boolean","description":"Ignored."},"platform_key":{"type":"boolean","description":"Ignored."}}}}}},"responses":{"200":{"description":"The model as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgModel"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"501":{"$ref":"#/components/responses/Disabled"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"delete":{"operationId":"deleteModel","tags":["models"],"summary":"Delete a model","description":"Administrators only. It also comes off every allow-list in the\norganisation. A model a filter or a router step still uses is\nrefused with 409.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AliasDeleted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/control/v1/models/{alias}/check":{"post":{"operationId":"checkModel","tags":["models"],"summary":"Check a model works","description":"Administrators only. Calls the model's own backend with a tool and a\nquestion only that tool can answer. A model that fails still answers\n200, with `ok` false. It can cost money.\n","parameters":[{"name":"alias","in":"path","required":true,"description":"Send a slash as `%2F`.","schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"What the check found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelProbe"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/mcp-servers":{"get":{"operationId":"listMCPServers","tags":["mcp-servers"],"summary":"List MCP servers","description":"Anyone in the organisation may read it. Only its administrators see\n`url` (empty for everyone else), `auth_header` and `has_api_key`.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"The organisation's MCP servers.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MCPServer"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/mcp-servers/{alias}":{"parameters":[{"name":"alias","in":"path","required":true,"description":"Lowercase letters, digits and inner hyphens, up to 64.","schema":{"type":"string"}}],"put":{"operationId":"putMCPServer","tags":["mcp-servers"],"summary":"Create or replace an MCP server","description":"Administrators only. See https://docs.keera.ch/manage/mcp/.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"The server's Streamable HTTP endpoint, http or https."},"description":{"type":"string"},"auth_header":{"type":"string","description":"The header the credential goes in. Empty means\n`Authorization`, as a bearer token; any other header gets\nthe credential as it is.\n"},"enabled":{"type":"boolean","description":"Left out, a new server starts on and an existing one keeps its state."},"api_key":{"type":"string","writeOnly":true,"description":"Left out keeps the stored one, and \"\" removes it. Needed\nagain when `url` moves to another host.\n"}}}}}},"responses":{"200":{"description":"The server as stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPServer"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}},"delete":{"operationId":"deleteMCPServer","tags":["mcp-servers"],"summary":"Delete an MCP server","description":"Administrators only. A server a guardrail's tool allow-list still\nnames is refused with 409.\n","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AliasDeleted"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/control/v1/tool-calls":{"get":{"operationId":"toolCalls","tags":["mcp-servers"],"summary":"List tool calls","description":"Administrators only, for one organisation. Newest first, a page at a\ntime. With `summary=1` it adds up the calls of each tool instead,\nmost called first, with no paging.\n","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"from","in":"query","description":"Defaults to 30 days ago.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`.","schema":{"type":"string"}},{"name":"project_id","in":"query","schema":{"type":"string"}},{"name":"key_id","in":"query","schema":{"type":"string"}},{"name":"user_id","in":"query","schema":{"type":"string"}},{"name":"server","in":"query","description":"An MCP server's alias.","schema":{"type":"string"}},{"name":"tool","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","description":"Rows per page. Defaults to 100, at most 5000.","schema":{"type":"integer","minimum":0}},{"name":"before","in":"query","description":"The `next_before` of the previous page.","schema":{"type":"integer","format":"int64"}},{"name":"summary","in":"query","description":"\"1\" adds up the calls per tool.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"The calls, or with `summary=1` the totals per tool.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","title":"Calls","required":["from","to","data"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ToolCallRow"}},"next_before":{"type":"integer","format":"int64","description":"Pass as `before` for the next page. Left out on an empty page."}}},{"type":"object","title":"Summary","required":["from","to","data"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ToolSummary"}}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/playground/chat":{"post":{"operationId":"playgroundChat","tags":["models"],"summary":"Send a chat from the panel","description":"Sends one OpenAI-shaped chat completion with one of the caller's own\nAPI keys, named by `key_id`. It goes through the same data plane as\n`POST /api/v1/chat/completions`: that key's guardrails, budgets and\nusage apply, and the answer is the same, streamed as server-sent\nevents when the body asks for `stream`. The operator key cannot use\nit, nor can a subscription key.\n","parameters":[{"name":"key_id","in":"query","required":true,"description":"One of the caller's own API keys.","schema":{"type":"string"}},{"name":"X-Keera-Client","in":"header","description":"Only `keera-chat` is kept, and only from a CLI sign-in. Every\nother request is recorded as `keera-playground`.\n","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"An OpenAI chat completion request.","required":["model","messages"],"properties":{"model":{"type":"string","description":"A model or router alias the key may call."},"messages":{"type":"array","items":{"type":"object"}},"stream":{"type":"boolean"}},"additionalProperties":true}}}},"responses":{"200":{"description":"The completion, as the inference API answers it.","content":{"application/json":{"schema":{"type":"object","description":"An OpenAI chat completion."}},"text/event-stream":{"schema":{"type":"string","description":"OpenAI chat completion chunks, ending with `data: [DONE]`."}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The model is on the deployment's key and the organisation has no credit left.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"502":{"description":"The model's backend failed or could not be reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/v1/connect":{"get":{"operationId":"listConnect","tags":["connect"],"summary":"List the clients to connect","description":"The coding agents that can be pointed at this gateway, with their\ntemplates, and the gateway address to put in them. Anyone signed in\nmay read it.\n","responses":{"200":{"description":"The client catalogue.","content":{"application/json":{"schema":{"type":"object","required":["data","gateway_url"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ConnectClient"}},"gateway_url":{"type":"string","format":"uri","description":"The inference API's base URL, as a browser reaches it."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/control/v1/connect/render":{"post":{"operationId":"renderConnect","tags":["connect"],"summary":"Fill in every client's configuration","description":"Renders each client's block for the given models, the default first.\nNo key is sent: a block that holds one carries `placeholder` instead.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"models":{"type":"array","description":"The models and routers the key may call, the default first.","items":{"$ref":"#/components/schemas/ConnectModel"}}}}}}},"responses":{"200":{"description":"One rendered block per client.","content":{"application/json":{"schema":{"type":"object","required":["data","placeholder"],"properties":{"data":{"type":"array","items":{"type":"object","required":["client","config","run"],"properties":{"client":{"type":"string","description":"The client's id."},"config":{"type":"string","description":"The filled-in block."},"run":{"type":"string","description":"What to type once it is configured."}}}},"placeholder":{"type":"string","description":"The text that stands in a block for the key.","examples":["keera_sk_…"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/control/v1/clients":{"get":{"operationId":"listClients","tags":["connect"],"summary":"List the caller's own clients","description":"Which clients called with the caller's own keys in the window, and\nwith which key, newest first. Even an administrator sees only their\nown. The window is widened to whole days. The operator key has no\nkeys, so it gets an empty list and `anonymous`.\n","parameters":[{"name":"from","in":"query","description":"Defaults to 30 days ago.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The clients.","content":{"application/json":{"schema":{"type":"object","required":["from","to","data"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ConnectedClient"}},"anonymous":{"type":"boolean","description":"The caller is not a person, so has no keys."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/control/v1/diagnostics":{"get":{"operationId":"diagnostics","tags":["reports"],"summary":"Check the deployment","description":"What a deployment is missing, as `keera doctor` shows it. Nothing is written. An administrator sees their organisation's checks; the deployment's own checks are only for operators.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"probe","in":"query","description":"`1` also calls each enabled model. Only for the organisation's administrators, and needs one organisation.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Diagnosis"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/setup":{"get":{"operationId":"setup","tags":["reports"],"summary":"Count what the deployment has","description":"For the panel's first-run steps. Without an organisation, an operator gets counts across all of them.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupState"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/overview":{"get":{"operationId":"overview","tags":["reports"],"summary":"Dashboard figures","description":"The dashboard in one call. Narrowed to a project, key, person or model, it is that thing's own screen. A member sees the whole organisation's, like an administrator.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"from","in":"query","description":"Start of the window. Default: 30 days before `to`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the window. Default: now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`. `from` wins over it.","schema":{"type":"string","examples":["24h"]}},{"name":"project_id","in":"query","description":"Only this project's requests.","schema":{"type":"string"}},{"name":"key_id","in":"query","description":"Only this key's requests.","schema":{"type":"string"}},{"name":"user_id","in":"query","description":"Only this person's requests.","schema":{"type":"string"}},{"name":"alias","in":"query","description":"Only this model, as clients name it.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["overview","project_names"],"properties":{"overview":{"$ref":"#/components/schemas/Overview"},"project_names":{"type":"object","additionalProperties":{"type":"string"},"description":"Project names by id."},"key_names":{"type":"object","additionalProperties":{"type":"string"},"description":"Key names by id. Only on a narrowed overview."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/map":{"get":{"operationId":"trafficMap","tags":["reports"],"summary":"Traffic between clients and models","description":"The deployment as a picture: the clients that called, every model of the organisation, and the traffic between them. A member sees only the traffic of their own keys.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"from","in":"query","description":"Start of the window. Default: 30 days before `to`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the window. Default: now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`. `from` wins over it.","schema":{"type":"string","examples":["24h"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["from","to","gateway_url","gateway","clients","models","flows","last_minute"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"gateway_url":{"type":"string","description":"The inference API's address, for the middle of the drawing."},"gateway":{"$ref":"#/components/schemas/UsageCell","description":"Every request in the window."},"clients":{"type":"array","items":{"$ref":"#/components/schemas/MapClient"},"description":"Busiest first."},"models":{"type":"array","items":{"$ref":"#/components/schemas/MapModel"},"description":"Every model of the organisation, busiest first."},"flows":{"type":"array","items":{"$ref":"#/components/schemas/MapFlow"}},"last_minute":{"type":"array","items":{"type":"integer","format":"int64"},"description":"Requests finished in each of the last 60 seconds, the most recent second first."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/usage":{"get":{"operationId":"usage","tags":["reports"],"summary":"Usage report","description":"Requests, tokens and cost per group. Over more than two days it counts whole days, and `from` and `to` say so. A member sees the whole organisation's, like an administrator.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"from","in":"query","description":"Start of the window. Default: 30 days before `to`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the window. Default: now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`. `from` wins over it.","schema":{"type":"string","examples":["24h"]}},{"name":"group_by","in":"query","description":"What each row is. Default `model`.","schema":{"type":"string","enum":["client","day","key","model","org","project","user"]}},{"name":"project_id","in":"query","description":"Only this project's requests.","schema":{"type":"string"}},{"name":"key_id","in":"query","description":"Only this key's requests.","schema":{"type":"string"}},{"name":"user_id","in":"query","description":"Only this person's requests.","schema":{"type":"string"}},{"name":"alias","in":"query","description":"Only this model, as clients name it.","schema":{"type":"string"}},{"name":"format","in":"query","description":"`csv` answers a spreadsheet instead.","schema":{"type":"string","enum":["csv"]}}],"responses":{"200":{"description":"The report, or with `format=csv` a spreadsheet.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ReportLabels"},{"type":"object","required":["from","to","data","chart"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"data":{"type":"array","items":{"$ref":"#/components/schemas/UsageBucket"}},"chart":{"$ref":"#/components/schemas/UsageChart"},"client_names":{"type":"object","additionalProperties":{"type":"string"},"description":"Client labels by client key. Only with `group_by=client`."}}}]}},"text/csv":{"schema":{"type":"string","description":"Columns: the group's name, its id, requests, input_tokens, output_tokens, cost, org_id, from, to."}}},"headers":{"Content-Disposition":{"description":"With `format=csv`: an attachment and its file name.","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/audit":{"get":{"operationId":"audit","tags":["reports"],"summary":"Audit log","description":"Who changed what, newest first. Administrators only. Without `from`, `to` or `since` it has no time window.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"actor","in":"query","description":"Only entries by this actor.","schema":{"type":"string"}},{"name":"action","in":"query","description":"Only this action, such as `billing.topup`.","schema":{"type":"string"}},{"name":"from","in":"query","description":"Start of the window. Default: 30 days before `to`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the window. Default: now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`. `from` wins over it.","schema":{"type":"string","examples":["24h"]}},{"name":"limit","in":"query","description":"Entries per page. Default 100, at most 5000.","schema":{"type":"integer","minimum":0}},{"name":"before","in":"query","description":"The `next_before` of the previous page.","schema":{"type":"integer","format":"int64"}},{"name":"format","in":"query","description":"`csv` answers a spreadsheet of every matching entry instead, up to 5000.","schema":{"type":"string","enum":["csv"]}}],"responses":{"200":{"description":"The report, or with `format=csv` a spreadsheet.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"}},"filters":{"$ref":"#/components/schemas/AuditFacets","description":"First page only."},"next_before":{"type":"integer","format":"int64","description":"Pass as `before` for the next page. Absent on an empty page."}}}},"text/csv":{"schema":{"type":"string","description":"Columns: timestamp, actor, action, target_type, target_id, detail."}}},"headers":{"Content-Disposition":{"description":"With `format=csv`: an attachment and its file name.","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/requests":{"get":{"operationId":"requests","tags":["reports"],"summary":"Request log","description":"One row per inference request, whatever happened to it. An administrator reads the whole organisation's; a member only the requests made with their own keys.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"from","in":"query","description":"Start of the window. Default: 30 days before `to`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the window. Default: now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`. `from` wins over it.","schema":{"type":"string","examples":["24h"]}},{"name":"project_id","in":"query","description":"Only this project's requests.","schema":{"type":"string"}},{"name":"key_id","in":"query","description":"Only this key's requests.","schema":{"type":"string"}},{"name":"user_id","in":"query","description":"Only this person's requests. A member may only name themselves.","schema":{"type":"string"}},{"name":"alias","in":"query","description":"Only this model, as clients name it.","schema":{"type":"string"}},{"name":"outcome","in":"query","description":"Only requests with this outcome. `unhappy` is everything that did not deliver.","schema":{"type":"string","enum":["ok","unhappy","failed","refused","interrupted"]}},{"name":"status","in":"query","description":"One exact status, such as `402`, or a class, such as `5xx`. Anything else filters nothing.","schema":{"type":"string","examples":["402","5xx"]}},{"name":"request_id","in":"query","description":"One request, by the id the gateway answered with. It ignores the window.","schema":{"type":"string"}},{"name":"sort","in":"query","description":"The order. Default `recent`, newest first.","schema":{"type":"string","enum":["recent","cost","tokens","ttft"]}},{"name":"limit","in":"query","description":"Rows per page. Default 100, at most 5000.","schema":{"type":"integer","minimum":0}},{"name":"before","in":"query","description":"The `next_before` of the previous page. Paging works only in the default order.","schema":{"type":"integer","format":"int64"}},{"name":"format","in":"query","description":"`csv` answers a spreadsheet of the whole filtered window instead, up to 5000 rows.","schema":{"type":"string","enum":["csv"]}},{"name":"facets","in":"query","description":"`1` adds `filters`, the values this window can be narrowed by. First page only.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"The report, or with `format=csv` a spreadsheet.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ReportLabels"},{"type":"object","required":["from","to","data"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RequestLogEntry"}},"outcomes":{"$ref":"#/components/schemas/RequestOutcomes","description":"Counts over the whole window. First page only."},"filters":{"$ref":"#/components/schemas/RequestFacets","description":"With `facets=1`, first page only."},"next_before":{"type":"integer","format":"int64","description":"Pass as `before` for the next page. Only in the default order, and absent on an empty page."}}}]}},"text/csv":{"schema":{"type":"string","description":"Columns: timestamp, model, status, error, key, key_id, project, user, input_tokens, output_tokens, cost, latency_ms, ttft_ms, stream, estimated, canceled, note."}}},"headers":{"Content-Disposition":{"description":"With `format=csv`: an attachment and its file name.","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/requests/stream":{"get":{"operationId":"requestStream","tags":["reports"],"summary":"Request log, live","description":"The rows `GET /control/v1/requests` returns, narrowed the same way, as they are written. The response stays open.\n\nEach batch is an event named `requests` whose `data` is a JSON object:\n\n- `data`: the new rows, newest first, at most 200 (`RequestLogEntry`).\n- `outcomes`: the counts over the window `since` gives (`RequestOutcomes`).\n- `next_after`: the newest id sent. Pass it as `after` to resume.\n- `project_names`, `key_names`, `user_names`, `user_full_names`: only when a row names something the earlier labels did not.\n\nAn idle stream sends a `: keepalive` comment every 25 seconds. It ends when the server shuts down; reconnect with `after`.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"since","in":"query","description":"The window the counts cover, as a duration back from now, such as `24h`. It moves with the clock. Default: 30 days.","schema":{"type":"string","examples":["24h"]}},{"name":"project_id","in":"query","description":"Only this project's requests.","schema":{"type":"string"}},{"name":"key_id","in":"query","description":"Only this key's requests.","schema":{"type":"string"}},{"name":"user_id","in":"query","description":"Only this person's requests. A member may only name themselves.","schema":{"type":"string"}},{"name":"alias","in":"query","description":"Only this model, as clients name it.","schema":{"type":"string"}},{"name":"outcome","in":"query","description":"Only requests with this outcome. `unhappy` is everything that did not deliver.","schema":{"type":"string","enum":["ok","unhappy","failed","refused","interrupted"]}},{"name":"status","in":"query","description":"One exact status, such as `402`, or a class, such as `5xx`. Anything else filters nothing.","schema":{"type":"string","examples":["402","5xx"]}},{"name":"request_id","in":"query","description":"One request, by the id the gateway answered with. It ignores the window.","schema":{"type":"string"}},{"name":"after","in":"query","description":"The newest id the reader already has. Default: the end of the log.","schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"A stream of server-sent events.","content":{"text/event-stream":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/sessions":{"get":{"operationId":"sessions","tags":["reports"],"summary":"Sessions","description":"The request log grouped into tasks. A member sees only the sessions of their own keys. See https://docs.keera.ch/monitor/requests-and-sessions/.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"from","in":"query","description":"Start of the window. Default: 30 days before `to`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the window. Default: now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"The window as a duration back from now, such as `24h`. `from` wins over it.","schema":{"type":"string","examples":["24h"]}},{"name":"project_id","in":"query","description":"Only this project's requests.","schema":{"type":"string"}},{"name":"key_id","in":"query","description":"Only this key's requests.","schema":{"type":"string"}},{"name":"user_id","in":"query","description":"Only this person's requests. A member may only name themselves.","schema":{"type":"string"}},{"name":"alias","in":"query","description":"Only this model, as clients name it.","schema":{"type":"string"}},{"name":"session_key","in":"query","description":"Only this conversation.","schema":{"type":"string"}},{"name":"outcome","in":"query","description":"Only sessions holding a request with this outcome. `ok` keeps those where every request was served.","schema":{"type":"string","enum":["ok","unhappy","failed","refused","interrupted"]}},{"name":"status","in":"query","description":"How the session ended: the last request's status, such as `402`, or a class, such as `5xx`.","schema":{"type":"string","examples":["402","5xx"]}},{"name":"unhappy","in":"query","description":"`1` is `outcome=unhappy`, for older `keera` commands.","schema":{"type":"string","enum":["1"]},"deprecated":true},{"name":"sort","in":"query","description":"The order. Default `recent`, newest first.","schema":{"type":"string","enum":["recent","cost","tokens","requests","duration"]}},{"name":"limit","in":"query","description":"Rows per page. Default 100, at most 5000.","schema":{"type":"integer","minimum":0}},{"name":"before","in":"query","description":"The `next_before` of the previous page. Paging works only in the default order.","schema":{"type":"integer","format":"int64"}},{"name":"format","in":"query","description":"`csv` answers a spreadsheet of the whole filtered window instead, up to 5000 sessions.","schema":{"type":"string","enum":["csv"]}},{"name":"facets","in":"query","description":"`1` adds `filters`, the values this window can be narrowed by. First page only.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"The report, or with `format=csv` a spreadsheet.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ReportLabels"},{"type":"object","required":["from","to","data","gap_seconds"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentSession"}},"gap_seconds":{"type":"integer","format":"int64","description":"The idle gap that cut these sessions."},"totals":{"$ref":"#/components/schemas/AgentSessionTotals","description":"Over the whole window. First page only."},"filters":{"$ref":"#/components/schemas/RequestFacets","description":"With `facets=1`, first page only."},"next_before":{"type":"integer","format":"int64","description":"Pass as `before` for the next page. Only in the default order, and absent on an empty page."}}}]}},"text/csv":{"schema":{"type":"string","description":"Columns: started, ended, minutes, requests, ok, failed, refused, interrupted, models, key, key_id, project, user, input_tokens, output_tokens, cost, last_status, last_error, session_id, named_by_client."}}},"headers":{"Content-Disposition":{"description":"With `format=csv`: an attachment and its file name.","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/sessions/{id}":{"get":{"operationId":"session","tags":["reports"],"summary":"One session","description":"One task from start to end. A member sees it only if every request in it was made with their own keys.","parameters":[{"name":"id","in":"path","required":true,"description":"The id of any request in the session.","schema":{"type":"integer","format":"int64"}},{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ReportLabels"},{"type":"object","required":["session","requests","tool_calls","gap_seconds"],"properties":{"session":{"$ref":"#/components/schemas/AgentSession"},"requests":{"type":"array","items":{"$ref":"#/components/schemas/RequestLogEntry"},"description":"Every request, in order."},"tool_calls":{"type":"array","items":{"$ref":"#/components/schemas/ToolCallRow"},"description":"The MCP tool calls in between, oldest first."},"gap_seconds":{"type":"integer","format":"int64","description":"The idle gap that cut this session."}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/billing":{"get":{"operationId":"billing","tags":["billing"],"summary":"Monthly bill","description":"One month of use of the deployment's own provider keys, per organisation and provider model, at list price. Administrators only. See https://docs.keera.ch/manage/billing/.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"month","in":"query","description":"The month, in UTC. Default: the current one.","schema":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}$","examples":["2026-09"]}},{"name":"format","in":"query","description":"`csv` answers a spreadsheet instead.","schema":{"type":"string","enum":["csv"]}}],"responses":{"200":{"description":"The report, or with `format=csv` a spreadsheet.","content":{"application/json":{"schema":{"type":"object","required":["month","from","to","data","total","payments"],"properties":{"month":{"type":"string","examples":["2026-09"]},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"data":{"type":"array","items":{"$ref":"#/components/schemas/BillingRow"}},"total":{"$ref":"#/components/schemas/BillingTotal"},"payments":{"type":"boolean","description":"Whether this deployment takes payments in advance."}}}},"text/csv":{"schema":{"type":"string","description":"Columns: month, org_id, org_name, provider, model, calls, input_tokens, cached_input_tokens, cache_write_tokens, output_tokens, amount, and from_credit when the deployment takes payments."}}},"headers":{"Content-Disposition":{"description":"With `format=csv`: an attachment and its file name.","schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/billing/account":{"get":{"operationId":"creditAccount","tags":["billing"],"summary":"Credit account","description":"The organisation's credit, its settings and its latest payments. Administrators only. It first looks up payments started in the last hour, so one just paid shows at once. On a deployment that takes no payments, it answers only `{\"payments\": false}`.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["payments"],"properties":{"payments":{"type":"boolean","description":"Whether this deployment takes payments. Every other field is there only when it does."},"vat_bp":{"type":"integer","format":"int64","description":"VAT added to every payment, in basis points."},"fee_fixed_micros":{"type":"integer","format":"int64","description":"The fixed part of the transaction fee."},"fee_bp":{"type":"integer","format":"int64","description":"The share of the credit added to the fee, in basis points."},"fee_min_micros":{"type":"integer","format":"int64","description":"The least the share adds to the fee."},"topup_min_micros":{"type":"integer","format":"int64","description":"The smallest payment."},"topup_max_micros":{"type":"integer","format":"int64","description":"The largest payment."},"account":{"$ref":"#/components/schemas/CreditAccount"},"recent":{"type":"array","items":{"$ref":"#/components/schemas/Payment"},"description":"The latest 20 payments, newest first."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"patch":{"operationId":"updateCreditAccount","tags":["billing"],"summary":"Change the automatic top-up","description":"Sets the automatic top-up, and for operators whether the organisation is billed by invoice. Administrators only. Changing the top-up clears why the last one failed.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"topup_below_micros":{"type":"integer","format":"int64","description":"Below this balance the saved card is charged. Send with `topup_micros`; both 0 turn the top-up off."},"topup_micros":{"type":"integer","format":"int64","description":"What one top-up pays, from CHF 1 in whole centimes, and at least `topup_below_micros`."},"invoiced":{"type":"boolean","description":"Bill by invoice, so no credit is needed. Operators only."}},"additionalProperties":false}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditAccount"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"description":"The body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/billing/account/card":{"delete":{"operationId":"forgetCard","tags":["billing"],"summary":"Remove the saved card","description":"Here and at PostFinance. The automatic top-up stops with it. Administrators only.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["org_id","deleted"],"properties":{"org_id":{"type":"string"},"deleted":{"type":"boolean","const":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/billing/topups":{"post":{"operationId":"createTopUp","tags":["billing"],"summary":"Pay for credit","description":"Starts a card payment and answers the payment page to send the person to. The credit is added once PostFinance says it is paid. VAT and a transaction fee come on top. Administrators only.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount_micros"],"properties":{"amount_micros":{"type":"integer","format":"int64","description":"The credit to buy, from CHF 1 to CHF 10,000, in whole centimes."},"save_card":{"type":"boolean","description":"Save the card for automatic top-ups."}},"additionalProperties":false}}}},"responses":{"201":{"description":"Started","content":{"application/json":{"schema":{"type":"object","required":["payment","url"],"properties":{"payment":{"$ref":"#/components/schemas/Payment","description":"The pending payment. Its `created_at` is not filled in."},"url":{"type":"string","format":"uri","description":"PostFinance Checkout's payment page."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"description":"The body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"},"502":{"description":"PostFinance Checkout could not start the payment. Nothing was charged. The code is `payment_unavailable`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/control/v1/billing/payments":{"get":{"operationId":"listPayments","tags":["billing"],"summary":"List payments","description":"Newest first. Administrators only; an operator without `org_id` sees every organisation's.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"limit","in":"query","description":"Payments per page. Default 50, at most 500.","schema":{"type":"integer","minimum":0}},{"name":"before","in":"query","description":"The `next_before` of the previous page: a payment id.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}},"next_before":{"type":"string","description":"Pass as `before` for the next page. Absent on an empty page."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/billing/payments/{id}/receipt":{"get":{"operationId":"paymentReceipt","tags":["billing"],"summary":"Receipt of a payment","description":"The invoice PostFinance renders for a paid payment. Administrators only. A grant has none.","parameters":[{"name":"id","in":"path","required":true,"description":"The payment id.","schema":{"type":"string"}}],"responses":{"200":{"description":"The receipt.","headers":{"Content-Disposition":{"description":"An attachment and its file name.","schema":{"type":"string"}}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The payment has no receipt: it is a grant, or not paid. The code is `no_receipt`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"},"502":{"description":"PostFinance Checkout did not hand out the receipt. The code is `receipt_unavailable`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/control/v1/billing/grants":{"post":{"operationId":"grantCredit","tags":["billing"],"summary":"Grant credit","description":"Adds credit without a payment, or takes some away: a trial, a refund, money that came by bank transfer. Operators only.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["org_id","amount_micros","note"],"properties":{"org_id":{"type":"string","description":"The organisation to credit."},"amount_micros":{"type":"integer","format":"int64","description":"The credit to add, or below 0 to take away. Not 0, and at most CHF 10,000 either way."},"note":{"type":"string","description":"Why. It shows in the organisation's payments."}},"additionalProperties":false}}}},"responses":{"201":{"description":"Granted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"description":"The body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/billing/postfinance":{"post":{"operationId":"postFinanceWebhook","tags":["billing"],"summary":"PostFinance webhook","description":"PostFinance Checkout says a transaction changed. The call proves nothing: the gateway only reads the transaction back over PostFinance's own API. Anything that is not one of its transactions is acknowledged and ignored. Throttled per address.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"entityId":{"type":"integer","format":"int64","description":"The transaction id."},"listenerEntityTechnicalName":{"type":"string","examples":["Transaction"]},"spaceId":{"type":"integer","format":"int64","description":"The PostFinance space."}}}}}},"responses":{"200":{"description":"Handled or ignored. No body."},"400":{"description":"The body is not JSON.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"This deployment takes no payments.","content":{"text/plain":{"schema":{"type":"string"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"description":"The transaction could not be read. PostFinance tries again later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/healthz":{"get":{"operationId":"healthz","tags":["probes"],"summary":"Liveness","security":[],"responses":{"200":{"description":"The process is up.","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","const":"ok"}}}}}}}}},"/readyz":{"get":{"operationId":"ready","tags":["probes"],"summary":"Readiness","description":"Ready when Postgres answers. The reason for a failure goes to the log, not the answer.","security":[],"responses":{"200":{"description":"Ready.","content":{"application/json":{"schema":{"type":"object","required":["status","usage_written","usage_dropped"],"properties":{"status":{"type":"string","const":"ok"},"usage_written":{"type":"integer","format":"int64","description":"Usage events this process has written."},"usage_dropped":{"type":"integer","format":"int64","description":"Usage events this process has dropped."}}}}}},"503":{"description":"The database did not answer.","content":{"application/json":{"schema":{"type":"object","required":["status","database"],"properties":{"status":{"type":"string","const":"degraded"},"database":{"type":"string","const":"unavailable"}}}}}}}}},"/metrics":{"get":{"operationId":"metricsRoute","tags":["probes"],"summary":"Prometheus metrics","description":"Metrics span every organisation, so only an operator may read them, or a scraper with the metrics token (`KEERA_METRICS_TOKEN`) as a bearer token. That token reads this route and nothing else.","security":[{"bearer":[]},{"session":[]}],"responses":{"200":{"description":"The Prometheus text exposition.","content":{"text/plain; version=0.0.4; charset=utf-8":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/sandbox-classes":{"get":{"tags":["sandboxes"],"operationId":"listSandboxClasses","summary":"List sandbox classes","description":"Open to anyone in the organisation. Also gives the driver, when there is one, and the sandbox limits of the organisation's first project, so a client can grey out what creating a sandbox would refuse.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","limits"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SandboxClass"}},"driver":{"type":"object","description":"Absent when the deployment runs no sandbox driver.","required":["name","isolation","tiers"],"properties":{"name":{"type":"string","examples":["kubernetes","podman"]},"isolation":{"$ref":"#/components/schemas/SandboxIsolation","description":"The strongest tier the driver can deliver."},"tiers":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SandboxIsolation"},"description":"Every tier the driver can deliver."}}},"limits":{"$ref":"#/components/schemas/ResolvedSandbox"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/control/v1/sandbox-classes/{name}":{"parameters":[{"name":"name","in":"path","required":true,"description":"The class name: lowercase letters, digits and hyphens.","schema":{"type":"string"}}],"put":{"tags":["sandboxes"],"operationId":"putSandboxClass","summary":"Create or replace a sandbox class","description":"Administrators only. The body has the shape the list returns, so a class read can be written back; `org_id`, `created_at` and `updated_at` are ignored and the name comes from the path. Zero or empty fields take their defaults (isolation `isolated`). An administrator may only use the images of the starting classes, those `KEERA_SANDBOX_IMAGES` allows, or the image the class already has.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["image"],"additionalProperties":false,"properties":{"org_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"image":{"type":"string"},"isolation":{"$ref":"#/components/schemas/SandboxIsolation"},"cpu_millis":{"type":"integer"},"memory_mib":{"type":"integer"},"disk_mib":{"type":"integer"},"default_ttl_seconds":{"type":"integer"},"max_ttl_seconds":{"type":"integer"},"warm":{"type":"integer"},"purposes":{"type":"array","items":{"$ref":"#/components/schemas/SandboxPurpose"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxClass"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Not an administrator, an image the caller may not use, or a warm pool for an organisation that has not paid yet (`org_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["sandboxes"],"operationId":"deleteSandboxClass","summary":"Delete a sandbox class","description":"Administrators only. A class in use can be deleted: running sandboxes keep their own copy of it. The answer says how many are still running.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["name","deleted","live_sandboxes"],"properties":{"name":{"type":"string"},"deleted":{"type":"boolean","const":true},"live_sandboxes":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/control/v1/sandboxes":{"get":{"tags":["sandboxes"],"operationId":"listSandboxes","summary":"List sandboxes","description":"A member sees only their own. An administrator sees the whole organisation's. Finished sandboxes are left out unless `all=1`.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"project_id","in":"query","description":"Only this project's.","schema":{"type":"string"}},{"name":"class","in":"query","description":"Only this class.","schema":{"type":"string"}},{"name":"purpose","in":"query","description":"Only engineer or agent sandboxes.","schema":{"$ref":"#/components/schemas/SandboxPurpose"}},{"name":"user_id","in":"query","description":"Only this person's. Administrators only; a member always gets their own.","schema":{"type":"string"}},{"name":"all","in":"query","description":"Include finished sandboxes. Only `1` counts.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Sandbox"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Another organisation, or an account that is not a person.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["sandboxes"],"operationId":"createSandbox","summary":"Create a sandbox","description":"The sandbox gets an API key of its own, in the project it lands in. A member cannot choose the project: it is the organisation's oldest. An organisation that signed itself up waits for its first payment. The answer comes before the machine is ready; its `state` is `pending`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","class"],"additionalProperties":false,"properties":{"org_id":{"type":"string","description":"Only an operator needs it."},"project_id":{"type":"string","description":"Administrators only."},"name":{"type":"string","description":"Lowercase letters, digits and interior hyphens. It becomes a hostname."},"class":{"type":"string"},"purpose":{"$ref":"#/components/schemas/SandboxPurpose","description":"Defaults to `engineer`."},"ttl_seconds":{"type":"integer","minimum":1,"description":"The lifetime asked for. Left out, the class default. Above a ceiling, it is cut down, not refused."},"repo":{"type":"string","description":"What to check out. An agent sandbox needs one."},"branch":{"type":"string"},"task":{"type":"string","description":"The instruction an agent sandbox starts on. It is never stored."},"authorized_keys":{"type":"array","items":{"type":"string"},"description":"The ssh public keys that may open a shell. An engineer's sandbox needs at least one."},"env":{"type":"object","additionalProperties":{"type":"string"},"description":"Extra environment. It cannot name a `KEERA_` variable."}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sandbox"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A member chose a project, or the organisation has not paid yet (`org_limited`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Refused: no such class, a name already in use, or over a quota (`sandbox_state`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/sandboxes/{ref}":{"parameters":[{"name":"ref","in":"path","required":true,"description":"The sandbox's id (`sbx_...`) or its name. A name is unique only for one person in one organisation, so an operator naming one also passes `org_id`.","schema":{"type":"string"}}],"get":{"tags":["sandboxes"],"operationId":"getSandbox","summary":"Get a sandbox","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sandbox"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"More than one person has a live sandbox of that name (`sandbox_ambiguous`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["sandboxes"],"operationId":"terminateSandbox","summary":"Terminate a sandbox","description":"The machine goes; the row stays, so its cost still resolves. An administrator may terminate any sandbox in their organisation.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id","name","terminated"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"terminated":{"type":"boolean","const":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The sandbox's state does not allow it, or the manager refused. The code is `sandbox_state`, `sandbox_not_ready` or `sandbox_ambiguous`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/sandboxes/{ref}/suspend":{"parameters":[{"name":"ref","in":"path","required":true,"description":"The sandbox's id (`sbx_...`) or its name. A name is unique only for one person in one organisation, so an operator naming one also passes `org_id`.","schema":{"type":"string"}}],"post":{"tags":["sandboxes"],"operationId":"suspendSandbox","summary":"Suspend a sandbox","description":"Releases the compute and keeps the volume. It finishes in the background, so the answer is the state read back now, not the state asked for.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sandbox"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The sandbox's state does not allow it, or the manager refused. The code is `sandbox_state`, `sandbox_not_ready` or `sandbox_ambiguous`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/sandboxes/{ref}/resume":{"parameters":[{"name":"ref","in":"path","required":true,"description":"The sandbox's id (`sbx_...`) or its name. A name is unique only for one person in one organisation, so an operator naming one also passes `org_id`.","schema":{"type":"string"}}],"post":{"tags":["sandboxes"],"operationId":"resumeSandbox","summary":"Resume a sandbox","description":"Brings back a suspended or expired sandbox. An expired one gets a new key and lifetime, under the guardrail as it is now. It finishes in the background, so the answer is the state read back now.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sandbox"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The sandbox's state does not allow it, or the manager refused. The code is `sandbox_state`, `sandbox_not_ready` or `sandbox_ambiguous`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/sandboxes/{ref}/extend":{"parameters":[{"name":"ref","in":"path","required":true,"description":"The sandbox's id (`sbx_...`) or its name. A name is unique only for one person in one organisation, so an operator naming one also passes `org_id`.","schema":{"type":"string"}}],"post":{"tags":["sandboxes"],"operationId":"extendSandbox","summary":"Extend a sandbox's lifetime","description":"Sets the expiry to now plus the lifetime asked for, cut down to the class's and the guardrail's ceilings. No body is the same as `{}`: the class default.","parameters":[{"$ref":"#/components/parameters/OrgID"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"ttl_seconds":{"type":"integer","minimum":1}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["id","name","expires_at"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The sandbox has expired (resume it instead) or is no longer live (`sandbox_state`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"}}}},"/control/v1/sandbox-usage":{"get":{"tags":["sandboxes"],"operationId":"sandboxUsage","summary":"Report sandbox usage","description":"Administrators only. Sandbox time grouped by class, project or person. A sandbox counts whole in the window it was created in. The window is the last 30 days unless `from`, `to` or `since` say otherwise.","parameters":[{"$ref":"#/components/parameters/OrgID"},{"name":"group_by","in":"query","description":"How to group. Defaults to `class`.","schema":{"type":"string","enum":["class","project","user"],"default":"class"}},{"name":"from","in":"query","description":"Start of the window, RFC 3339.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of the window, RFC 3339. Defaults to now.","schema":{"type":"string","format":"date-time"}},{"name":"since","in":"query","description":"A duration back from now, such as `24h`. `from` wins over it.","schema":{"type":"string","examples":["24h"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["data","group_by","from","to"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SandboxUsage"}},"group_by":{"type":"string","enum":["class","project","user"]},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/sandbox/v1/{ref}/tcp/{port}":{"get":{"tags":["sandboxes"],"operationId":"attachSandbox","summary":"Open a byte stream to a sandbox port","description":"An HTTP/1.1 upgrade, not JSON. The gateway authenticates the caller as the control API does, dials the port inside the sandbox, answers `101` and copies bytes both ways until one end stops. A connection idle for two hours is closed. Port 2222 is the sandbox's ssh server; `keera sandbox ssh` and `keera sandbox proxy` are the clients.\n\nOnly the sandbox's owner or an operator may attach. An administrator can see and terminate a colleague's sandbox but not open it.","parameters":[{"name":"ref","in":"path","required":true,"description":"The sandbox's id (`sbx_...`) or its name. A name is unique only for one person in one organisation, so an operator naming one also passes `org_id`.","schema":{"type":"string"}},{"name":"port","in":"path","required":true,"description":"An unprivileged port inside the sandbox. 2222 is ssh.","schema":{"type":"integer","minimum":1024,"maximum":65535,"examples":[2222]}},{"$ref":"#/components/parameters/OrgID"},{"name":"Upgrade","in":"header","required":true,"description":"The protocol token. It is versioned, because the `keera` command may be older than the gateway.","schema":{"type":"string","const":"keera-sandbox/1"}},{"name":"Connection","in":"header","required":true,"schema":{"type":"string","const":"Upgrade"}}],"responses":{"101":{"description":"Switching protocols. From here on the connection carries the port's raw bytes.","headers":{"Upgrade":{"schema":{"type":"string","const":"keera-sandbox/1"}},"Connection":{"schema":{"type":"string","const":"Upgrade"}}}},"400":{"description":"The port is not between 1024 and 65535, or an operator named a sandbox without an organisation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The sandbox is somebody else's.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The sandbox's state does not allow it, or the manager refused. The code is `sandbox_state`, `sandbox_not_ready` or `sandbox_ambiguous`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"426":{"description":"The `Upgrade` header is missing or names another protocol (`upgrade_required`). A proxy in front may have stripped it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"$ref":"#/components/responses/Disabled"}}}},"/sandbox/v1/git-credential":{"post":{"tags":["sandboxes"],"operationId":"gitCredential","summary":"Get a fresh repository credential","description":"Asked for by a sandbox, not a person: `git-credential-keera` calls it when the token it holds has five minutes left. It authenticates with the sandbox's own API key (`KEERA_API_KEY`) as a bearer token, so it stops working when the sandbox ends or its owner is disabled. The replaced token is revoked where the forge allows it. There is no request body.\n\nThe answer is in the format git's credential helpers speak. It takes 10 requests a minute per client address on each replica.","security":[{"bearer":[]}],"responses":{"200":{"description":"The credential, as `key=value` lines. `password_expiry_utc` is a Unix time, and only there when the token runs out.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"text/plain":{"schema":{"type":"string"},"example":"username=x-access-token\npassword=ghs_...\npassword_expiry_utc=1767225600\n"}}},"401":{"description":"No key, or a key that belongs to no live sandbox.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Nothing to refresh: the sandbox has no repository, is not running or has run out of time (`sandbox_state`), or the forge gave no token (`no_git_credential`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"501":{"$ref":"#/components/responses/Disabled"}}}},"/api/v1/chat/completions":{"post":{"tags":["inference"],"operationId":"createChatCompletion","summary":"Create a chat completion","externalDocs":{"description":"OpenAI Chat Completions","url":"https://platform.openai.com/docs/api-reference/chat/create"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"X-Request-Id","in":"header","description":"An id for the request. Kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`, otherwise replaced.","schema":{"type":"string"}},{"name":"X-Client-Request-Id","in":"header","description":"The client's own id, as OpenAI's API takes it: printable ASCII, up to 512 characters. It is logged and sent on to OpenAI models. An invalid one is ignored.","schema":{"type":"string"}},{"name":"X-Keera-Client","in":"header","description":"The client's own name, which wins over the `User-Agent` in reports.","schema":{"type":"string"}},{"name":"Content-Encoding","in":"header","description":"The body may be gzip or deflate.","schema":{"type":"string","enum":["identity","gzip","x-gzip","deflate"]}},{"name":"X-Keera-Session","in":"header","description":"Names the session (task) this request belongs to, instead of the one the gateway derives from the opening prompt. `X-Session-Id`, `Helicone-Session-Id` and `X-Claude-Code-Session-Id` are read too; the first with a value wins. See https://docs.keera.ch/monitor/requests-and-sessions/.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","messages"],"additionalProperties":true,"properties":{"model":{"type":"string","description":"A model or router alias from `GET /api/v1/models`."},"messages":{"type":"array","items":{"type":"object","additionalProperties":true}},"stream":{"type":"boolean","default":false,"description":"Answer as server-sent events (`text/event-stream`)."},"max_tokens":{"type":"integer"},"max_completion_tokens":{"type":"integer"},"tools":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"responses":{"200":{"description":"The provider's answer, in this API's format.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"X-Keera-Model":{"description":"The alias that answered; behind a router, the destination that answered.","schema":{"type":"string"}},"X-Keera-Router":{"description":"The router and where it sent the request, as `router -\u003e model`, with ` (fallback)` or ` (every destination failed)` when so.","schema":{"type":"string"}},"X-Keera-Filters":{"description":"The filters that acted on the request, in order, comma-separated.","schema":{"type":"string"}},"X-Keera-Removed-Tools":{"description":"Hosted tools taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Removed-Fields":{"description":"Fields taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Budget-Remaining":{"description":"What is left to spend at the tightest level with a budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Limit":{"description":"That level's budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Reset":{"description":"When that budget resets.","schema":{"type":"string","format":"date-time"}},"X-Keera-Budget-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"X-RateLimit-Limit-Requests":{"description":"Requests per minute at the tightest level that limits them.","schema":{"type":"integer"}},"X-RateLimit-Remaining-Requests":{"description":"Requests left in that bucket.","schema":{"type":"integer"}},"X-Keera-RateLimit-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"Retry-After":{"description":"Passed on from the provider.","schema":{"type":"integer"}},"X-Should-Retry":{"description":"Passed on from the provider.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"text/event-stream":{"schema":{"type":"string","description":"Server-sent events, when the request set `stream: true`."}}}},"400":{"description":"The body or a field is not valid. Codes include `invalid_body`, `missing_model`, `context_length_exceeded` and `unsupported_parameter`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"A budget is spent (`budget_exceeded`), or the organisation's prepaid credit for the deployment's provider keys is (`credit_exhausted`). Not 429: it does not lift by waiting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Refused by a filter (`filter_refused`), a model the organisation may not use until it has paid (`org_limited`), or a subscription key while the deployment takes none (`subscriptions_off`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such model, or the key may not use it (`model_not_found`). The message says where a model of another kind is served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is over the gateway's limit (`request_too_large`, 32 MiB by default), or too long for a filter's model (`filter_input_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"A `Content-Encoding` other than gzip or deflate (`unsupported_encoding`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"502":{"description":"The model's backend could not be reached or gave an answer that could not be read (`upstream_unavailable`, `upstream_error`), or a filter failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No backend serves the model (`no_backend`), a router or filter could not decide, or the control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error status the model's provider gave, such as 504 or 529, passed on in the caller's error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"OpenAI's Chat Completions API, for any chat model or router the key may use, whatever provider serves it."}},"/api/v1/completions":{"post":{"tags":["inference"],"operationId":"createCompletion","summary":"Create a text completion","externalDocs":{"description":"OpenAI Completions","url":"https://platform.openai.com/docs/api-reference/completions/create"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"X-Request-Id","in":"header","description":"An id for the request. Kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`, otherwise replaced.","schema":{"type":"string"}},{"name":"X-Client-Request-Id","in":"header","description":"The client's own id, as OpenAI's API takes it: printable ASCII, up to 512 characters. It is logged and sent on to OpenAI models. An invalid one is ignored.","schema":{"type":"string"}},{"name":"X-Keera-Client","in":"header","description":"The client's own name, which wins over the `User-Agent` in reports.","schema":{"type":"string"}},{"name":"Content-Encoding","in":"header","description":"The body may be gzip or deflate.","schema":{"type":"string","enum":["identity","gzip","x-gzip","deflate"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","prompt"],"additionalProperties":true,"properties":{"model":{"type":"string","description":"A model or router alias from `GET /api/v1/models`."},"prompt":{"oneOf":[{"type":"string"},{"type":"array"}]},"stream":{"type":"boolean","default":false,"description":"Answer as server-sent events (`text/event-stream`)."},"max_tokens":{"type":"integer"}}}}}},"responses":{"200":{"description":"The provider's answer, in this API's format.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"X-Keera-Model":{"description":"The alias that answered; behind a router, the destination that answered.","schema":{"type":"string"}},"X-Keera-Router":{"description":"The router and where it sent the request, as `router -\u003e model`, with ` (fallback)` or ` (every destination failed)` when so.","schema":{"type":"string"}},"X-Keera-Filters":{"description":"The filters that acted on the request, in order, comma-separated.","schema":{"type":"string"}},"X-Keera-Removed-Tools":{"description":"Hosted tools taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Removed-Fields":{"description":"Fields taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Budget-Remaining":{"description":"What is left to spend at the tightest level with a budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Limit":{"description":"That level's budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Reset":{"description":"When that budget resets.","schema":{"type":"string","format":"date-time"}},"X-Keera-Budget-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"X-RateLimit-Limit-Requests":{"description":"Requests per minute at the tightest level that limits them.","schema":{"type":"integer"}},"X-RateLimit-Remaining-Requests":{"description":"Requests left in that bucket.","schema":{"type":"integer"}},"X-Keera-RateLimit-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"Retry-After":{"description":"Passed on from the provider.","schema":{"type":"integer"}},"X-Should-Retry":{"description":"Passed on from the provider.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"text/event-stream":{"schema":{"type":"string","description":"Server-sent events, when the request set `stream: true`."}}}},"400":{"description":"The body or a field is not valid. Codes include `invalid_body`, `missing_model`, `context_length_exceeded` and `unsupported_parameter`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"A budget is spent (`budget_exceeded`), or the organisation's prepaid credit for the deployment's provider keys is (`credit_exhausted`). Not 429: it does not lift by waiting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Refused by a filter (`filter_refused`), a model the organisation may not use until it has paid (`org_limited`), or a subscription key while the deployment takes none (`subscriptions_off`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such model, or the key may not use it (`model_not_found`). The message says where a model of another kind is served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is over the gateway's limit (`request_too_large`, 32 MiB by default), or too long for a filter's model (`filter_input_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"A `Content-Encoding` other than gzip or deflate (`unsupported_encoding`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"502":{"description":"The model's backend could not be reached or gave an answer that could not be read (`upstream_unavailable`, `upstream_error`), or a filter failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No backend serves the model (`no_backend`), a router or filter could not decide, or the control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error status the model's provider gave, such as 504 or 529, passed on in the caller's error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"OpenAI's legacy Completions API, for completion models."}},"/api/v1/embeddings":{"post":{"tags":["inference"],"operationId":"createEmbedding","summary":"Create embeddings","externalDocs":{"description":"OpenAI Embeddings","url":"https://platform.openai.com/docs/api-reference/embeddings/create"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"X-Request-Id","in":"header","description":"An id for the request. Kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`, otherwise replaced.","schema":{"type":"string"}},{"name":"X-Client-Request-Id","in":"header","description":"The client's own id, as OpenAI's API takes it: printable ASCII, up to 512 characters. It is logged and sent on to OpenAI models. An invalid one is ignored.","schema":{"type":"string"}},{"name":"X-Keera-Client","in":"header","description":"The client's own name, which wins over the `User-Agent` in reports.","schema":{"type":"string"}},{"name":"Content-Encoding","in":"header","description":"The body may be gzip or deflate.","schema":{"type":"string","enum":["identity","gzip","x-gzip","deflate"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","input"],"additionalProperties":true,"properties":{"model":{"type":"string","description":"A model or router alias from `GET /api/v1/models`."},"input":{"oneOf":[{"type":"string"},{"type":"array"}]}}}}}},"responses":{"200":{"description":"The provider's answer, in this API's format.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"X-Keera-Model":{"description":"The alias that answered; behind a router, the destination that answered.","schema":{"type":"string"}},"X-Keera-Router":{"description":"The router and where it sent the request, as `router -\u003e model`, with ` (fallback)` or ` (every destination failed)` when so.","schema":{"type":"string"}},"X-Keera-Filters":{"description":"The filters that acted on the request, in order, comma-separated.","schema":{"type":"string"}},"X-Keera-Removed-Tools":{"description":"Hosted tools taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Removed-Fields":{"description":"Fields taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Budget-Remaining":{"description":"What is left to spend at the tightest level with a budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Limit":{"description":"That level's budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Reset":{"description":"When that budget resets.","schema":{"type":"string","format":"date-time"}},"X-Keera-Budget-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"X-RateLimit-Limit-Requests":{"description":"Requests per minute at the tightest level that limits them.","schema":{"type":"integer"}},"X-RateLimit-Remaining-Requests":{"description":"Requests left in that bucket.","schema":{"type":"integer"}},"X-Keera-RateLimit-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"Retry-After":{"description":"Passed on from the provider.","schema":{"type":"integer"}},"X-Should-Retry":{"description":"Passed on from the provider.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"The body or a field is not valid. Codes include `invalid_body`, `missing_model`, `context_length_exceeded` and `unsupported_parameter`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"A budget is spent (`budget_exceeded`), or the organisation's prepaid credit for the deployment's provider keys is (`credit_exhausted`). Not 429: it does not lift by waiting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Refused by a filter (`filter_refused`), a model the organisation may not use until it has paid (`org_limited`), or a subscription key while the deployment takes none (`subscriptions_off`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such model, or the key may not use it (`model_not_found`). The message says where a model of another kind is served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is over the gateway's limit (`request_too_large`, 32 MiB by default), or too long for a filter's model (`filter_input_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"A `Content-Encoding` other than gzip or deflate (`unsupported_encoding`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"502":{"description":"The model's backend could not be reached or gave an answer that could not be read (`upstream_unavailable`, `upstream_error`), or a filter failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No backend serves the model (`no_backend`), a router or filter could not decide, or the control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error status the model's provider gave, such as 504 or 529, passed on in the caller's error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"For embedding models."}},"/api/v1/messages":{"post":{"tags":["inference"],"operationId":"createMessage","summary":"Create a message","externalDocs":{"description":"Anthropic Messages","url":"https://docs.anthropic.com/en/api/messages"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"X-Request-Id","in":"header","description":"An id for the request. Kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`, otherwise replaced.","schema":{"type":"string"}},{"name":"X-Client-Request-Id","in":"header","description":"The client's own id, as OpenAI's API takes it: printable ASCII, up to 512 characters. It is logged and sent on to OpenAI models. An invalid one is ignored.","schema":{"type":"string"}},{"name":"X-Keera-Client","in":"header","description":"The client's own name, which wins over the `User-Agent` in reports.","schema":{"type":"string"}},{"name":"Content-Encoding","in":"header","description":"The body may be gzip or deflate.","schema":{"type":"string","enum":["identity","gzip","x-gzip","deflate"]}},{"name":"X-Keera-Session","in":"header","description":"Names the session (task) this request belongs to, instead of the one the gateway derives from the opening prompt. `X-Session-Id`, `Helicone-Session-Id` and `X-Claude-Code-Session-Id` are read too; the first with a value wins. See https://docs.keera.ch/monitor/requests-and-sessions/.","schema":{"type":"string"}},{"name":"anthropic-version","in":"header","description":"Passed on to Anthropic models. Defaults to the version the gateway knows.","schema":{"type":"string"}},{"name":"anthropic-beta","in":"header","description":"Beta flags, passed on to Anthropic models.","schema":{"type":"string"}},{"name":"beta","in":"query","description":"Sent by Anthropic's SDKs; ignored.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","messages","max_tokens"],"additionalProperties":true,"properties":{"model":{"type":"string","description":"A model or router alias from `GET /api/v1/models`."},"messages":{"type":"array","items":{"type":"object","additionalProperties":true}},"system":{"oneOf":[{"type":"string"},{"type":"array"}]},"max_tokens":{"type":"integer"},"stream":{"type":"boolean","default":false,"description":"Answer as server-sent events (`text/event-stream`)."},"tools":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"responses":{"200":{"description":"The provider's answer, in this API's format.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"X-Keera-Model":{"description":"The alias that answered; behind a router, the destination that answered.","schema":{"type":"string"}},"X-Keera-Router":{"description":"The router and where it sent the request, as `router -\u003e model`, with ` (fallback)` or ` (every destination failed)` when so.","schema":{"type":"string"}},"X-Keera-Filters":{"description":"The filters that acted on the request, in order, comma-separated.","schema":{"type":"string"}},"X-Keera-Removed-Tools":{"description":"Hosted tools taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Removed-Fields":{"description":"Fields taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Budget-Remaining":{"description":"What is left to spend at the tightest level with a budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Limit":{"description":"That level's budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Reset":{"description":"When that budget resets.","schema":{"type":"string","format":"date-time"}},"X-Keera-Budget-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"X-RateLimit-Limit-Requests":{"description":"Requests per minute at the tightest level that limits them.","schema":{"type":"integer"}},"X-RateLimit-Remaining-Requests":{"description":"Requests left in that bucket.","schema":{"type":"integer"}},"X-Keera-RateLimit-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"Retry-After":{"description":"Passed on from the provider.","schema":{"type":"integer"}},"X-Should-Retry":{"description":"Passed on from the provider.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"text/event-stream":{"schema":{"type":"string","description":"Server-sent events, when the request set `stream: true`."}}}},"400":{"description":"The body or a field is not valid. Codes include `invalid_body`, `missing_model`, `context_length_exceeded`, `unsupported_parameter`, and `subscription_model` for a subscription model on an API other than this one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No valid key, or a subscription model without a Claude sign-in (`missing_claude_sign_in`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"A budget is spent (`budget_exceeded`), or the organisation's prepaid credit for the deployment's provider keys is (`credit_exhausted`). Not 429: it does not lift by waiting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Refused by a filter (`filter_refused`), a model the organisation may not use until it has paid (`org_limited`), or a subscription key while the deployment takes none (`subscriptions_off`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such model, or the key may not use it (`model_not_found`). The message says where a model of another kind is served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is over the gateway's limit (`request_too_large`, 32 MiB by default), or too long for a filter's model (`filter_input_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"A `Content-Encoding` other than gzip or deflate (`unsupported_encoding`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"502":{"description":"The model's backend could not be reached or gave an answer that could not be read (`upstream_unavailable`, `upstream_error`), or a filter failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No backend serves the model (`no_backend`), a router or filter could not decide, or the control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error status the model's provider gave, such as 504 or 529, passed on in the caller's error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Anthropic's Messages API: the same chat models, routers and guardrails as chat completions, in Anthropic's format, translated where the model is not Anthropic's. Errors come in Anthropic's shape, with `type: error`.\n\nFor a subscription model, Claude Code sends the caller's Claude sign-in in `Authorization` and the Keera key in `X-Keera-Key`; the gateway passes Anthropic's `anthropic-ratelimit-unified-*` headers back. See https://docs.keera.ch/operate/claude-plans/."}},"/api/v1/messages/count_tokens":{"post":{"tags":["inference"],"operationId":"countTokens","summary":"Count a message's tokens","description":"Answered by the gateway from an estimate, never forwarded: forwarding would send the prompt to Anthropic without its filters. The estimate errs high and includes tools and the system prompts the guardrails and routers add. The model is checked as a real request would check it.","externalDocs":{"description":"Anthropic Count Message tokens","url":"https://docs.anthropic.com/en/api/messages-count-tokens"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"X-Request-Id","in":"header","description":"An id for the request. Kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`, otherwise replaced.","schema":{"type":"string"}},{"name":"X-Client-Request-Id","in":"header","description":"The client's own id, as OpenAI's API takes it: printable ASCII, up to 512 characters. It is logged and sent on to OpenAI models. An invalid one is ignored.","schema":{"type":"string"}},{"name":"X-Keera-Client","in":"header","description":"The client's own name, which wins over the `User-Agent` in reports.","schema":{"type":"string"}},{"name":"Content-Encoding","in":"header","description":"The body may be gzip or deflate.","schema":{"type":"string","enum":["identity","gzip","x-gzip","deflate"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","messages"],"additionalProperties":true,"properties":{"model":{"type":"string","description":"A model or router alias from `GET /api/v1/models`."},"messages":{"type":"array","items":{"type":"object","additionalProperties":true}},"system":{"oneOf":[{"type":"string"},{"type":"array"}]},"tools":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"responses":{"200":{"description":"The estimate.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["input_tokens"],"properties":{"input_tokens":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A model the organisation may not use until it has paid (`org_limited`), or a subscription key while the deployment takes none.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such model, or the key may not use it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is over the gateway's limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"A `Content-Encoding` other than gzip or deflate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/responses":{"post":{"tags":["inference"],"operationId":"createResponse","summary":"Create a response","externalDocs":{"description":"OpenAI Responses","url":"https://platform.openai.com/docs/api-reference/responses/create"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"X-Request-Id","in":"header","description":"An id for the request. Kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`, otherwise replaced.","schema":{"type":"string"}},{"name":"X-Client-Request-Id","in":"header","description":"The client's own id, as OpenAI's API takes it: printable ASCII, up to 512 characters. It is logged and sent on to OpenAI models. An invalid one is ignored.","schema":{"type":"string"}},{"name":"X-Keera-Client","in":"header","description":"The client's own name, which wins over the `User-Agent` in reports.","schema":{"type":"string"}},{"name":"Content-Encoding","in":"header","description":"The body may be gzip or deflate.","schema":{"type":"string","enum":["identity","gzip","x-gzip","deflate"]}},{"name":"X-Keera-Session","in":"header","description":"Names the session (task) this request belongs to, instead of the one the gateway derives from the opening prompt. `X-Session-Id`, `Helicone-Session-Id` and `X-Claude-Code-Session-Id` are read too; the first with a value wins. See https://docs.keera.ch/monitor/requests-and-sessions/.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","input"],"additionalProperties":true,"properties":{"model":{"type":"string","description":"A model or router alias from `GET /api/v1/models`."},"input":{"oneOf":[{"type":"string"},{"type":"array"}]},"instructions":{"type":"string"},"max_output_tokens":{"type":"integer"},"stream":{"type":"boolean","default":false,"description":"Answer as server-sent events (`text/event-stream`)."},"tools":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"responses":{"200":{"description":"The provider's answer, in this API's format.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"X-Keera-Model":{"description":"The alias that answered; behind a router, the destination that answered.","schema":{"type":"string"}},"X-Keera-Router":{"description":"The router and where it sent the request, as `router -\u003e model`, with ` (fallback)` or ` (every destination failed)` when so.","schema":{"type":"string"}},"X-Keera-Filters":{"description":"The filters that acted on the request, in order, comma-separated.","schema":{"type":"string"}},"X-Keera-Removed-Tools":{"description":"Hosted tools taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Removed-Fields":{"description":"Fields taken out on the deployment's key.","schema":{"type":"string"}},"X-Keera-Budget-Remaining":{"description":"What is left to spend at the tightest level with a budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Limit":{"description":"That level's budget, in CHF.","schema":{"type":"string"}},"X-Keera-Budget-Reset":{"description":"When that budget resets.","schema":{"type":"string","format":"date-time"}},"X-Keera-Budget-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"X-RateLimit-Limit-Requests":{"description":"Requests per minute at the tightest level that limits them.","schema":{"type":"integer"}},"X-RateLimit-Remaining-Requests":{"description":"Requests left in that bucket.","schema":{"type":"integer"}},"X-Keera-RateLimit-Scope":{"description":"The level: `org`, `project` or `key`.","schema":{"type":"string","enum":["org","project","key"]}},"Retry-After":{"description":"Passed on from the provider.","schema":{"type":"integer"}},"X-Should-Retry":{"description":"Passed on from the provider.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"text/event-stream":{"schema":{"type":"string","description":"Server-sent events, when the request set `stream: true`."}}}},"400":{"description":"The body or a field is not valid. Codes include `invalid_body`, `missing_model`, `context_length_exceeded` and `unsupported_parameter`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"A budget is spent (`budget_exceeded`), or the organisation's prepaid credit for the deployment's provider keys is (`credit_exhausted`). Not 429: it does not lift by waiting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Refused by a filter (`filter_refused`), a model the organisation may not use until it has paid (`org_limited`), or a subscription key while the deployment takes none (`subscriptions_off`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such model, or the key may not use it (`model_not_found`). The message says where a model of another kind is served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is over the gateway's limit (`request_too_large`, 32 MiB by default), or too long for a filter's model (`filter_input_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"A `Content-Encoding` other than gzip or deflate (`unsupported_encoding`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"502":{"description":"The model's backend could not be reached or gave an answer that could not be read (`upstream_unavailable`, `upstream_error`), or a filter failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No backend serves the model (`no_backend`), a router or filter could not decide, or the control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error status the model's provider gave, such as 504 or 529, passed on in the caller's error shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"OpenAI's Responses API: the same chat models, routers and guardrails, translated where the model is not OpenAI's. On the deployment's key, `store` is turned off."}},"/api/v1/models":{"get":{"tags":["inference"],"operationId":"listInferenceModels","summary":"List models","description":"The models and routers this key may use, in a list both OpenAI's and Anthropic's SDKs read. It is never paged. Locked and decision models are left out.","externalDocs":{"description":"OpenAI List models","url":"https://platform.openai.com/docs/api-reference/models/list"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceModelList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A subscription key while the deployment takes none.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/models/{alias}":{"get":{"tags":["inference"],"operationId":"getInferenceModel","summary":"Get a model","description":"The alias may hold slashes, as in `anthropic/claude-sonnet-4-5`, escaped or not: the rest of the path is the alias.","externalDocs":{"description":"OpenAI Retrieve model","url":"https://platform.openai.com/docs/api-reference/models/retrieve"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"alias","in":"path","required":true,"description":"A model or router alias. It may contain `/`.","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InferenceModel"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"A subscription key while the deployment takes none.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such model, or the key may not use it (`model_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/api/hello":{"head":{"tags":["inference"],"operationId":"hello","summary":"Warm-up probe","description":"Some Anthropic-shaped clients send this before their first request. The path is doubled because they append `/api/hello` to a base URL that already ends in `/api`. It always answers 200.","security":[],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}}}}}}},"/api/mcp/{alias}":{"parameters":[{"name":"alias","in":"path","required":true,"description":"The MCP server's alias.","schema":{"type":"string"}}],"post":{"tags":["mcp"],"operationId":"sendMcpMessage","summary":"Send MCP messages","description":"MCP's Streamable HTTP transport, behind the API key. The body is one JSON-RPC message, or a batch. The gateway lists only the tools the key may call and refuses the others, runs the key's filters over a tool call's arguments, presents the server's own credential and records each call. Everything else is passed through.\n\nA refusal of the gateway's own is a JSON-RPC error with status 200, such as a tool the key may not call, a tool call without an id, or a tool call inside a batch. A key that may call only some tools may send only `initialize`, `ping`, `tools/*`, `logging/setLevel` and `tasks/*` other than `tasks/list`. The client sends `Accept: application/json, text/event-stream`, as MCP asks.","externalDocs":{"description":"MCP Streamable HTTP transport","url":"https://modelcontextprotocol.io/specification/2025-06-18/basic/transports"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"Mcp-Session-Id","in":"header","description":"The MCP session, passed to the server and back.","schema":{"type":"string"}},{"name":"Mcp-Protocol-Version","in":"header","description":"Passed to the server.","schema":{"type":"string"}},{"name":"Mcp-Method","in":"header","description":"Must match the message's method when sent.","schema":{"type":"string"}},{"name":"Mcp-Name","in":"header","description":"Must match the tool name or URI in the message when sent.","schema":{"type":"string"}},{"name":"X-Keera-Session","in":"header","description":"Names the session (task) this request belongs to, instead of the one the gateway derives from the opening prompt. `X-Session-Id`, `Helicone-Session-Id` and `X-Claude-Code-Session-Id` are read too; the first with a value wins. See https://docs.keera.ch/monitor/requests-and-sessions/.","schema":{"type":"string"}},{"name":"X-Keera-Client","in":"header","description":"The client's own name, which wins over the `User-Agent`.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/JsonRpcMessage"},{"type":"array","items":{"$ref":"#/components/schemas/JsonRpcMessage"}}]}}}},"responses":{"200":{"description":"The server's answer, or a JSON-RPC error from the gateway.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"Mcp-Session-Id":{"description":"The session the server gave, passed back.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/JsonRpcMessage"},{"type":"array","items":{"$ref":"#/components/schemas/JsonRpcMessage"}}]}},"text/event-stream":{"schema":{"type":"string","description":"JSON-RPC messages as server-sent events."}}}},"202":{"description":"Accepted: the body held only notifications or responses.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"Mcp-Session-Id":{"description":"The session the server gave, passed back.","schema":{"type":"string"}}}},"400":{"description":"The `Mcp-Method` or `Mcp-Name` header does not match the message (JSON-RPC error -32020), or the body could not be read.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/JsonRpcMessage"},{"$ref":"#/components/schemas/Error"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The server is in a private network and the organisation has not paid yet (`org_limited`), or a subscription key while the deployment takes none.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such server, it is disabled, or the key may not call any of its tools (`mcp_server_not_found`). Subscription keys reach no MCP server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The server could not be reached or read (`mcp_unavailable`), or it refused the gateway's credential for it (`mcp_credential_refused`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The body is over the gateway's limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"A `Content-Encoding` other than gzip or deflate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other status the server gave, passed on. A body that is neither JSON nor an event stream is dropped."}}},"get":{"tags":["mcp"],"operationId":"openMcpStream","summary":"Open the server's event stream","description":"Passed through to the server, which may answer 405 if it has no stream.","externalDocs":{"description":"MCP Streamable HTTP transport","url":"https://modelcontextprotocol.io/specification/2025-06-18/basic/transports"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"Mcp-Session-Id","in":"header","description":"The MCP session, passed to the server and back.","schema":{"type":"string"}},{"name":"Mcp-Protocol-Version","in":"header","description":"Passed to the server.","schema":{"type":"string"}},{"name":"Last-Event-Id","in":"header","description":"Resumes a stream, passed to the server.","schema":{"type":"string"}}],"responses":{"200":{"description":"The server's own messages.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"Mcp-Session-Id":{"description":"The session the server gave, passed back.","schema":{"type":"string"}}},"content":{"text/event-stream":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The server is in a private network and the organisation has not paid yet (`org_limited`), or a subscription key while the deployment takes none.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such server, it is disabled, or the key may not call any of its tools (`mcp_server_not_found`). Subscription keys reach no MCP server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The server could not be reached or read (`mcp_unavailable`), or it refused the gateway's credential for it (`mcp_credential_refused`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other status the server gave, passed on."}}},"delete":{"tags":["mcp"],"operationId":"endMcpSession","summary":"End an MCP session","description":"Passed through to the server.","externalDocs":{"description":"MCP Streamable HTTP transport","url":"https://modelcontextprotocol.io/specification/2025-06-18/basic/transports"},"security":[{"bearer":[]},{"apiKey":[]},{"keeraKey":[]}],"parameters":[{"name":"Mcp-Session-Id","in":"header","description":"The MCP session, passed to the server and back.","schema":{"type":"string"}},{"name":"Mcp-Protocol-Version","in":"header","description":"Passed to the server.","schema":{"type":"string"}}],"responses":{"200":{"description":"Ended.","headers":{"X-Request-Id":{"description":"The request's id. One the client sent is kept if it is up to 64 of `A-Z a-z 0-9 . _ : -`.","schema":{"type":"string"}},"Mcp-Session-Id":{"description":"The session the server gave, passed back.","schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The server is in a private network and the organisation has not paid yet (`org_limited`), or a subscription key while the deployment takes none.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such server, it is disabled, or the key may not call any of its tools (`mcp_server_not_found`). Subscription keys reach no MCP server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The server could not be reached or read (`mcp_unavailable`), or it refused the gateway's credential for it (`mcp_credential_refused`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The control plane is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other status the server gave, such as 204 or 405, passed on."}}}}},"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"On `/control`: the operator key, or a CLI token from `keera login`\n(`keera_cli_...`). On `/api`: an API key.\n"},"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"An API key, the way Anthropic clients send it."},"keeraKey":{"type":"apiKey","in":"header","name":"X-Keera-Key","description":"An API key, when `Authorization` carries the caller's own Claude\nsign-in instead. See https://docs.keera.ch/operate/claude-plans/.\n"},"session":{"type":"apiKey","in":"cookie","name":"keera_session","description":"The panel's sign-in. A request that changes something must also send\nthe `X-CSRF-Token` header, with the token from `GET /control/v1/me`.\n"}},"parameters":{"OrgID":{"name":"org_id","in":"query","description":"The organisation to act on. Only an operator, who belongs to none,\nneeds it; everyone else acts on their own.\n","schema":{"type":"string"}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"type":{"type":"string","description":"\"error\", on answers Anthropic clients also read."},"error":{"type":"object","required":["message","type"],"properties":{"message":{"type":"string"},"type":{"type":"string","examples":["invalid_request_error"]},"code":{"type":"string"}}}}},"SignInProvider":{"type":"object","description":"An identity provider the gateway signs people in through.","required":["name","label","signup","password"],"properties":{"name":{"type":"string","description":"What `provider` names it by."},"label":{"type":"string","description":"What a person is shown."},"signup":{"type":"boolean","description":"Whether somebody new may create an organisation through it."},"password":{"type":"boolean","description":"Whether it asks for a password, as an LDAP directory does, instead of redirecting."}}},"SignInRedirect":{"type":"object","description":"Where the panel goes next. For `keera login`, the loopback address with the code.","required":["redirect"],"properties":{"redirect":{"type":"string"}}},"User":{"type":"object","description":"A person in one organisation.","required":["id","org_id","email","role","created_at"],"properties":{"id":{"type":"string"},"org_id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"external_id":{"type":"string","description":"`\u003cprovider\u003e:\u003csubject\u003e`. Empty until their first sign-in."},"role":{"type":"string","enum":["operator","admin","member"]},"created_at":{"type":"string","format":"date-time"},"disabled_at":{"type":"string","format":"date-time","description":"Set while the person is turned off."},"last_active_at":{"type":"string","format":"date-time","description":"Their last sign-in or request. Only in lists."}}},"Membership":{"type":"object","description":"One organisation a person is in, or was invited to.","required":["user_id","org_id","org_name","role"],"properties":{"user_id":{"type":"string"},"org_id":{"type":"string"},"org_name":{"type":"string"},"role":{"type":"string","enum":["operator","admin","member"]},"invited":{"type":"boolean","description":"Not joined yet. Switching to it joins."}}},"Passkey":{"type":"object","required":["id","user_id","name","created_at"],"properties":{"id":{"type":"string"},"user_id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time"}}},"PasskeyLink":{"type":"object","description":"A one-time set-up link. It holds the token, so it is shown once and never stored.","required":["url","expires_at"],"properties":{"url":{"type":"string","format":"uri"},"expires_at":{"type":"string","format":"date-time"}}},"RegistrationOptions":{"type":"object","required":["challenge_id","email","options"],"properties":{"challenge_id":{"type":"string","description":"Send it back with the answer."},"email":{"type":"string","description":"The account the passkey is for."},"options":{"$ref":"#/components/schemas/CreationOptions"}}},"CreationOptions":{"type":"object","description":"What `navigator.credentials.create` is called with. Binary values are\nbase64url.\n","required":["challenge","rp","user","pubKeyCredParams","timeout","excludeCredentials","authenticatorSelection","attestation"],"properties":{"challenge":{"type":"string"},"rp":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"user":{"type":"object","required":["id","name","displayName"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"displayName":{"type":"string"}}},"pubKeyCredParams":{"type":"array","items":{"type":"object","required":["type","alg"],"properties":{"type":{"type":"string","const":"public-key"},"alg":{"type":"integer","description":"A COSE algorithm, such as -7 for ES256."}}}},"timeout":{"type":"integer","description":"Milliseconds."},"excludeCredentials":{"type":"array","description":"The passkeys the person already has.","items":{"$ref":"#/components/schemas/CredentialRef"}},"authenticatorSelection":{"type":"object","required":["residentKey","requireResidentKey","userVerification"],"properties":{"residentKey":{"type":"string"},"requireResidentKey":{"type":"boolean"},"userVerification":{"type":"string"}}},"attestation":{"type":"string"}}},"RequestOptions":{"type":"object","description":"What `navigator.credentials.get` is called with. It lists no\ncredentials, so the browser offers every passkey it has for this\ngateway.\n","required":["challenge","rpId","timeout","userVerification","allowCredentials"],"properties":{"challenge":{"type":"string"},"rpId":{"type":"string"},"timeout":{"type":"integer","description":"Milliseconds."},"userVerification":{"type":"string"},"allowCredentials":{"type":"array","items":{"$ref":"#/components/schemas/CredentialRef"}}}},"CredentialRef":{"type":"object","required":["type","id"],"properties":{"type":{"type":"string","const":"public-key"},"id":{"type":"string","description":"base64url."}}},"Registration":{"type":"object","description":"The browser's answer to `navigator.credentials.create`. Binary values are base64url.","required":["id","client_data_json","authenticator_data","public_key","algorithm"],"properties":{"id":{"type":"string"},"client_data_json":{"type":"string"},"authenticator_data":{"type":"string"},"public_key":{"type":"string","description":"The SubjectPublicKeyInfo, as `getPublicKey()` returns it."},"algorithm":{"type":"integer","description":"The COSE algorithm, as `getPublicKeyAlgorithm()` returns it."}}},"Assertion":{"type":"object","description":"The browser's answer to `navigator.credentials.get`. Binary values are base64url.","required":["id","client_data_json","authenticator_data","signature"],"properties":{"id":{"type":"string"},"client_data_json":{"type":"string"},"authenticator_data":{"type":"string"},"signature":{"type":"string"},"user_handle":{"type":"string"}}},"AccessKey":{"description":"One of the caller's keys, with what decides whether it works.","allOf":[{"$ref":"#/components/schemas/KeySummary"},{"type":"object","required":["state","scopes"],"properties":{"project_name":{"type":"string"},"state":{"type":"string","enum":["active","pending","expired","revoked"]},"scopes":{"type":"array","description":"The levels whose limits apply, organisation first.","items":{"$ref":"#/components/schemas/AccessScope"}},"allowed_models":{"type":"array","description":"The models and routers the key may call.","items":{"type":"string"}}}}]},"AccessScope":{"type":"object","description":"One level above a key, with its limits and what is used of them.","required":["type","id"],"properties":{"type":{"$ref":"#/components/schemas/ScopeType"},"id":{"type":"string"},"name":{"type":"string"},"rates":{"type":"array","items":{"$ref":"#/components/schemas/AccessRate"}},"budgets":{"type":"array","items":{"$ref":"#/components/schemas/AccessBudget"}},"system_prompt":{"type":"string","description":"This level's own instruction, not the joined chain."},"filters":{"type":"array","description":"This level's own filters.","items":{"type":"string"}}}},"AccessRate":{"type":"object","description":"One rate limit. Per day or month, the `_used` fields say how much is gone.","required":["period"],"properties":{"requests":{"type":"integer"},"tokens":{"type":"integer"},"period":{"type":"string","enum":["minute","day","month"]},"requests_used":{"type":"integer"},"tokens_used":{"type":"integer"}}},"AccessBudget":{"type":"object","required":["budget_micros","spend_micros","period","resets_at"],"properties":{"budget_micros":{"type":"integer","description":"Millionths of the currency unit."},"spend_micros":{"type":"integer"},"period":{"$ref":"#/components/schemas/Period"},"resets_at":{"type":"string","format":"date-time"}}},"Refusal":{"type":"object","description":"A request the gateway turned away. The status is the reason: 402 a\nspent budget, 429 a rate limit, 404 a model the key may not use.\n","required":["created_at","status"],"properties":{"created_at":{"type":"string","format":"date-time"},"alias":{"type":"string"},"status":{"type":"integer"},"key_id":{"type":"string"},"error":{"type":"string","description":"What the client was told."}}},"Org":{"type":"object","required":["id","name","created_at"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"email_domain":{"type":"string","description":"Places a first sign-in from this domain here."},"limited":{"type":"boolean","description":"It signed up and has not paid yet. Its models may not run on the\ndeployment's own machines, and it gets no sandboxes.\n"},"created_at":{"type":"string","format":"date-time"}}},"Project":{"type":"object","required":["id","org_id","name","description","created_at"],"properties":{"id":{"type":"string"},"org_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"ProjectSummary":{"description":"A project with its guardrails and its spend.","allOf":[{"$ref":"#/components/schemas/Project"},{"type":"object","required":["limits","active_keys","spend_micros","budget_micros","period","resets_at"],"properties":{"limits":{"$ref":"#/components/schemas/Limits"},"active_keys":{"type":"integer"},"spend_micros":{"type":"integer","format":"int64","description":"Spend this period, in millionths of the currency."},"budget_micros":{"type":"integer","format":"int64","description":"The budget that will stop the project's keys first, or 0 for\nnone.\n"},"period":{"$ref":"#/components/schemas/Period"},"resets_at":{"type":"string","format":"date-time","description":"When the period starts again."}}}]},"KeyKind":{"type":"string","enum":["standard","subscription"],"description":"`standard` reaches every model its guardrails allow. `subscription`\nreaches only models paid by the person's own Claude subscription.\nDefaults to `standard`.\n"},"KeyInfo":{"type":"object","description":"An issued key, without its secret.","required":["id","org_id","name","prefix","kind","created_at"],"properties":{"id":{"type":"string"},"org_id":{"type":"string"},"project_id":{"type":"string"},"user_id":{"type":"string","description":"The person whose spend this is."},"name":{"type":"string"},"prefix":{"type":"string","description":"The start of the secret, to recognise it by. Empty while pending."},"kind":{"$ref":"#/components/schemas/KeyKind"},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"revoked_at":{"type":"string","format":"date-time"},"pending":{"type":"boolean","description":"Waiting to be activated. It has no secret yet."}}},"KeySummary":{"description":"A key with its own guardrails, last use and spend.","allOf":[{"$ref":"#/components/schemas/KeyInfo"},{"type":"object","required":["limits","requests","spend_micros","subscription_micros"],"properties":{"limits":{"$ref":"#/components/schemas/Limits"},"last_used_at":{"type":"string","format":"date-time"},"requests":{"type":"integer","format":"int64","description":"Requests served."},"spend_micros":{"type":"integer","format":"int64"},"subscription_micros":{"type":"integer","format":"int64","description":"For a subscription key, what the person's Claude plan paid for, at API prices."},"plan":{"$ref":"#/components/schemas/PlanUsage"}}}]},"PlanUsage":{"type":"object","description":"How much of a Claude plan's usage limit is used, as Anthropic reports it.","required":["updated_at"],"properties":{"five_hour":{"type":"number","minimum":0,"maximum":1},"five_hour_resets_at":{"type":"string","format":"date-time"},"seven_day":{"type":"number","minimum":0,"maximum":1},"seven_day_resets_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["allowed","allowed_warning","rejected"]},"updated_at":{"type":"string","format":"date-time"}}},"Period":{"type":"string","enum":["day","month"]},"ScopeType":{"type":"string","enum":["org","project","key"]},"Limits":{"type":"object","additionalProperties":false,"description":"One scope's guardrail. A field left out or null inherits from the\nlevel above. For the lists, an empty list allows nothing and null\ninherits, so answers always carry them. The sandbox fields may not be\nset on a key.\n","properties":{"allowed_models":{"type":["array","null"],"items":{"type":"string"},"description":"Model aliases this scope may use."},"rate_limits":{"type":["array","null"],"description":"At most one per period.","items":{"$ref":"#/components/schemas/RateLimit"}},"budgets":{"type":["array","null"],"description":"At most one per period.","items":{"$ref":"#/components/schemas/Budget"}},"system_prompt":{"type":["string","null"],"maxLength":16384,"description":"Put ahead of every chat request's own messages, after those of the\nlevels above. At most 16 KiB.\n"},"filters":{"type":["array","null"],"items":{"type":"string"},"description":"Filter aliases, in the order they run, after those of the levels above."},"allowed_tools":{"type":["array","null"],"items":{"type":"string"},"description":"MCP tools this scope may call. An MCP server's alias for all its tools, or `alias/tool` for one."},"max_sandboxes":{"type":["integer","null"],"description":"Live sandboxes at once. 0 is no limit."},"max_sandbox_ttl_seconds":{"type":["integer","null"],"description":"Longest lifetime of one sandbox. 0 is no limit."},"sandbox_classes":{"type":["array","null"],"items":{"type":"string"},"description":"Sandbox classes this scope may use."},"max_sandbox_cpu_millis":{"type":["integer","null"],"description":"Most CPU for one sandbox, in millicores. 0 is no limit."},"max_sandbox_memory_mib":{"type":["integer","null"],"description":"Most memory for one sandbox, in MiB. 0 is no limit."},"allowed_repos":{"type":["array","null"],"items":{"type":"string"},"description":"Repositories a sandbox may check out: `owner/repo`, `owner` for\nall under it, or `*`. Where no level sets it, none may be. On an\norganisation, only an operator may change it.\n","examples":[["bankb","other/core"]]}}},"RateLimit":{"type":"object","additionalProperties":false,"description":"The most a scope may use per period. Set `requests`, `tokens` or both.","properties":{"requests":{"type":"integer","format":"int64","minimum":1},"tokens":{"type":"integer","format":"int64","minimum":1},"period":{"type":"string","enum":["minute","day","month"],"default":"minute"}}},"Budget":{"type":"object","additionalProperties":false,"description":"The most a scope may spend per period.","required":["budget_micros"],"properties":{"budget_micros":{"type":"integer","format":"int64","minimum":1,"description":"In millionths of the currency."},"period":{"type":"string","enum":["day","month"],"default":"month"}}},"Scope":{"type":"object","description":"One level's limits as the gateway enforces them.","required":["type","id","rpm","tpm","budget_micros","period"],"properties":{"type":{"$ref":"#/components/schemas/ScopeType"},"id":{"type":"string"},"rpm":{"type":"integer","description":"Requests per minute. 0 is no limit."},"tpm":{"type":"integer","description":"Tokens per minute. 0 is no limit."},"budget_micros":{"type":"integer","format":"int64","description":"0 is no limit."},"period":{"$ref":"#/components/schemas/Period"},"meter":{"type":"string","enum":["requests","tokens"],"description":"What the period counts. Absent is money."},"quota":{"type":"integer","format":"int64","description":"The most `meter` may count. 0 is no limit."}}},"ResolvedSandbox":{"type":"object","description":"What a sandbox may be: the organisation's guardrail, narrowed by the project's. Zero means unlimited.","required":["max_sandboxes","max_sandbox_ttl_seconds","sandbox_classes","max_sandbox_cpu_millis","max_sandbox_memory_mib","allowed_repos"],"properties":{"max_sandboxes":{"type":"integer"},"max_sandbox_ttl_seconds":{"type":"integer"},"sandbox_classes":{"type":["array","null"],"items":{"type":"string"},"description":"Null allows every class."},"max_sandbox_cpu_millis":{"type":"integer"},"max_sandbox_memory_mib":{"type":"integer"},"allowed_repos":{"type":["array","null"],"items":{"type":"string"},"description":"Null allows no repository, not every one."}}},"EffectiveLevel":{"type":"object","description":"One level above a scope, with what it sets on its own.","required":["type","id","limits"],"properties":{"type":{"$ref":"#/components/schemas/ScopeType"},"id":{"type":"string"},"name":{"type":"string"},"limits":{"$ref":"#/components/schemas/Limits"}}},"Effective":{"type":"object","description":"What a request against a scope meets. Rate limits and budgets stay per\nlevel in `scopes`, because each level is enforced on its own.\n","required":["type","id","levels","allowed_models","allowed_tools","scopes","sandbox"],"properties":{"type":{"$ref":"#/components/schemas/ScopeType"},"id":{"type":"string"},"levels":{"type":"array","description":"Organisation, then project, then key, as far as they apply.","items":{"$ref":"#/components/schemas/EffectiveLevel"}},"allowed_models":{"type":["array","null"],"items":{"type":"string"},"description":"null is every enabled model. An empty list is none."},"system_prompt":{"type":"string","description":"Every level's prompt joined, outermost first."},"filters":{"type":"array","items":{"type":"string"},"description":"Every level's filters, in the order they run."},"allowed_tools":{"type":["array","null"],"items":{"type":"string"},"description":"null is every tool of every MCP server."},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/Scope"}},"sandbox":{"$ref":"#/components/schemas/ResolvedSandbox"}}},"AliasDeleted":{"type":"object","description":"The answer to deleting a filter, router, model or MCP server.","required":["alias","deleted"],"properties":{"alias":{"type":"string"},"deleted":{"type":"boolean","const":true}}},"Filter":{"type":"object","description":"Reads a request before it is forwarded, and may change or stop it.\nSee https://docs.keera.ch/manage/filters/.\n","required":["org_id","alias","model","mode","prompt","created_at","updated_at"],"properties":{"org_id":{"type":"string"},"alias":{"type":"string"},"model":{"type":"string","description":"The model it runs on. Empty on a pattern filter."},"mode":{"type":"string","enum":["rewrite","gate","pattern"]},"prompt":{"type":"string","description":"The instruction for its model. Empty on a pattern filter."},"rules":{"type":"array","description":"A pattern filter's rules, applied in order.","items":{"$ref":"#/components/schemas/FilterRule"}},"shadow":{"type":"boolean","description":"Runs and records, but enforces nothing."},"description":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"FilterRule":{"type":"object","description":"One rule of a pattern filter. It either replaces or refuses.","required":["pattern"],"properties":{"pattern":{"type":"string","maxLength":512,"description":"An RE2 expression, as Go's regexp reads it. It may not match the empty string."},"replace":{"type":"string","description":"Put in literally for each match; `$1` is not expanded."},"refuse":{"type":"boolean","description":"Refuse the request when this matches, instead of replacing."},"reason":{"type":"string","maxLength":240,"description":"The sentence a refusal gives the sender. Only with `refuse`."}}},"FilterStat":{"type":"object","description":"One filter's traffic over a window.","required":["filter","runs","passed","rewrote","refused","errored","shadowed","latency_median_ms","latency_p95_ms","cost_micros","segments","changed"],"properties":{"filter":{"type":"string"},"runs":{"type":"integer","format":"int64"},"passed":{"type":"integer","format":"int64"},"rewrote":{"type":"integer","format":"int64"},"refused":{"type":"integer","format":"int64"},"errored":{"type":"integer","format":"int64"},"shadowed":{"type":"integer","format":"int64","description":"How many runs enforced nothing."},"latency_median_ms":{"type":"integer","format":"int64"},"latency_p95_ms":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"segments":{"type":"integer","format":"int64"},"changed":{"type":"integer","format":"int64"},"last_run_at":{"type":"string","format":"date-time"},"first_run_at":{"type":"string","format":"date-time"}}},"FilterPoint":{"type":"object","description":"One bucket of a filter's chart.","required":["at","runs","refused","errored","rewrote","cost_micros"],"properties":{"at":{"type":"string","format":"date-time"},"runs":{"type":"integer","format":"int64"},"refused":{"type":"integer","format":"int64"},"errored":{"type":"integer","format":"int64"},"rewrote":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"}}},"FilterProjectRow":{"type":"object","description":"One project's runs of a filter.","required":["project_id","runs","refused","rewrote","errored","cost_micros"],"properties":{"project_id":{"type":"string","description":"Empty for requests outside any project."},"runs":{"type":"integer","format":"int64"},"refused":{"type":"integer","format":"int64"},"rewrote":{"type":"integer","format":"int64"},"errored":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"}}},"FilterReport":{"description":"One filter's own screen.","allOf":[{"$ref":"#/components/schemas/FilterStat"},{"type":"object","required":["from","to","bucket","series","projects","requests","org_cost_micros"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"bucket":{"type":"string","enum":["hour","day"]},"series":{"type":"array","items":{"$ref":"#/components/schemas/FilterPoint"}},"projects":{"type":"array","description":"Most refused first.","items":{"$ref":"#/components/schemas/FilterProjectRow"}},"requests":{"type":"integer","format":"int64","description":"The organisation's requests in the window."},"org_cost_micros":{"type":"integer","format":"int64","description":"The organisation's cost in the window."}}}]},"FilterProbe":{"type":"object","description":"What one filter check found. A rewrite filter fills `segments`, a gate\n`verdicts`, a pattern filter `hits`.\n","required":["alias","mode","model","ok"],"properties":{"alias":{"type":"string"},"mode":{"type":"string","enum":["rewrite","gate","pattern"]},"model":{"type":"string"},"shadow":{"type":"boolean"},"segments":{"type":"array","items":{"$ref":"#/components/schemas/FilterSegment"}},"total_ms":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"error":{"type":"string"},"refused":{"type":"boolean","description":"It refused the sample instead of rewriting it."},"refusal":{"type":"string","description":"Its sentence when it refused."},"verdicts":{"type":"array","items":{"$ref":"#/components/schemas/FilterVerdict"}},"hits":{"type":"array","items":{"$ref":"#/components/schemas/FilterPatternHit"}},"free":{"type":"boolean","description":"The run cost nothing and used no GPU."},"warnings":{"type":"array","items":{"type":"string"}},"ok":{"type":"boolean"}}},"FilterSegment":{"type":"object","description":"One sample text, before and after.","required":["before","after","changed"],"properties":{"before":{"type":"string"},"after":{"type":"string"},"changed":{"type":"boolean"}}},"FilterVerdict":{"type":"object","description":"One gate run over half of the sample.","required":["sent","refused","expect_refusal"],"properties":{"sent":{"type":"array","items":{"type":"string"}},"refused":{"type":"boolean"},"reason":{"type":"string"},"expect_refusal":{"type":"boolean"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Left out when the backend served no logprobs."}}},"FilterPatternHit":{"type":"object","description":"One pattern rule that matched the sample.","required":["rule","matches"],"properties":{"rule":{"type":"string","description":"The expression that matched."},"matches":{"type":"integer"},"refused":{"type":"boolean"}}},"Router":{"type":"object","description":"A tree of steps a request goes through. See https://docs.keera.ch/manage/routers/.","required":["org_id","alias","start","created_at","updated_at"],"properties":{"org_id":{"type":"string"},"alias":{"type":"string"},"display_name":{"type":"string"},"description":{"type":"string"},"start":{"$ref":"#/components/schemas/RouterStep"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"SavedRouter":{"description":"A router as saved, with what to look at.","allOf":[{"$ref":"#/components/schemas/Router"},{"type":"object","properties":{"warning":{"type":"string","description":"What the organisation's allow-list now does with the router."},"warnings":{"type":"array","description":"Likely mistakes in the router itself.","items":{"type":"string"}}}}]},"RouterStep":{"type":"object","description":"One step of a router. Which fields it may set depends on `kind`:\n\n- `filter` and `model` name what they use in `ref`.\n- `route` has `mode`, `destinations` and `branches`; an instruction\n  step also `model`, `prompt` and `fallback`, a size step `ceilings`.\n- `rate_limit` has `requests`, `tokens` and `period`; `budget` has\n  `budget_micros` and `period`. Both may have `exceeded`.\n- `system_prompt` has `prompt`.\n\nEvery step but `route` and `model` needs `next`. A router has at most\n64 steps.\n","required":["id","kind"],"properties":{"id":{"type":"string","maxLength":32,"description":"Unique in the router. Lowercase letters, digits and inner hyphens."},"kind":{"type":"string","enum":["filter","route","rate_limit","budget","model","system_prompt"]},"ref":{"type":"string","description":"The filter or model alias a `filter` or `model` step uses."},"requests":{"type":"integer","format":"int64","description":"A rate limit's requests per period. 0 means no limit."},"tokens":{"type":"integer","format":"int64","description":"A rate limit's tokens per period. 0 means no limit."},"budget_micros":{"type":"integer","format":"int64","description":"A budget's most spend per period."},"prompt":{"type":"string","description":"A system prompt step's text (up to 16 KiB), or an instruction\nstep's instruction (up to 4 KiB).\n"},"mode":{"type":"string","enum":["instruction","fallback","latency","least-busy","size"],"description":"A route step's way of choosing. Defaults to instruction."},"model":{"type":"string","description":"The model an instruction step decides with."},"destinations":{"type":"array","maxItems":16,"description":"The models a route step chooses between.","items":{"type":"string"}},"ceilings":{"type":"object","description":"A size step's largest request, in estimated tokens, per\ndestination. Exactly one destination has none.\n","additionalProperties":{"type":"integer"}},"fallback":{"type":"string","description":"Where an instruction step sends what it could not decide. Empty refuses it."},"period":{"type":"string","enum":["minute","day","month"],"description":"What a limit counts over. A rate limit defaults to minute; a\nbudget takes day or month and defaults to month.\n"},"next":{"$ref":"#/components/schemas/RouterStep"},"exceeded":{"$ref":"#/components/schemas/RouterStep"},"branches":{"type":"object","description":"A route step's next steps, by destination. A destination with none\nis sent the request straight away.\n","additionalProperties":{"$ref":"#/components/schemas/RouterStep"}}}},"UsageCell":{"type":"object","description":"Traffic added up over a window.","required":["requests","input_tokens","output_tokens","cost_micros","refused","failed","ttft_median_ms"],"properties":{"requests":{"type":"integer","format":"int64"},"input_tokens":{"type":"integer","format":"int64"},"output_tokens":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"refused":{"type":"integer","format":"int64","description":"Every 4xx answer."},"failed":{"type":"integer","format":"int64","description":"Every 5xx answer."},"ttft_median_ms":{"type":"integer","format":"int64"}}},"RouterStat":{"description":"One router's headline for the list.","allOf":[{"$ref":"#/components/schemas/UsageCell"},{"type":"object","required":["chose","fell_back","errored"],"properties":{"chose":{"type":"integer","format":"int64"},"fell_back":{"type":"integer","format":"int64"},"errored":{"type":"integer","format":"int64"}}}]},"RouterDestination":{"description":"Where one router sent its traffic.","allOf":[{"type":"object","required":["alias"],"properties":{"alias":{"type":"string"}}},{"$ref":"#/components/schemas/UsageCell"}]},"RouterReport":{"type":"object","description":"One router's own screen.","required":["router","total","destinations","chose","fell_back","errored","decision_median_ms","decision_p95_ms"],"properties":{"router":{"type":"string"},"total":{"$ref":"#/components/schemas/UsageCell"},"destinations":{"type":"array","description":"Most used first.","items":{"$ref":"#/components/schemas/RouterDestination"}},"chose":{"type":"integer","format":"int64"},"fell_back":{"type":"integer","format":"int64"},"errored":{"type":"integer","format":"int64"},"decision_median_ms":{"type":"integer","format":"int64"},"decision_p95_ms":{"type":"integer","format":"int64"}}},"RouterCheck":{"type":"object","required":["router","routes"],"properties":{"router":{"type":"string"},"routes":{"type":"array","items":{"$ref":"#/components/schemas/RouteProbe"}}}},"RouteProbe":{"type":"object","description":"What checking one route step found. An instruction step fills\n`decisions`; the other modes fill `chain`.\n","required":["step","mode","destinations","ok"],"properties":{"step":{"type":"string"},"mode":{"type":"string","enum":["instruction","fallback","latency","least-busy","size"]},"model":{"type":"string"},"destinations":{"type":"array","description":"What the step offered when checked.","items":{"type":"string"}},"dropped":{"type":"array","description":"Destinations that were not offered, and why.","items":{"type":"string"}},"decisions":{"type":"array","items":{"$ref":"#/components/schemas/RouteSampleDecision"}},"chain":{"type":"array","items":{"$ref":"#/components/schemas/RouteHop"}},"total_ms":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"error":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"ok":{"type":"boolean"}}},"RouteSampleDecision":{"type":"object","description":"One sample prompt and where the step sent it.","required":["prompt","asks"],"properties":{"prompt":{"type":"string"},"asks":{"type":"string","description":"What the sample tests."},"chosen":{"type":"string"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Left out when the backend served no logprobs."},"error":{"type":"string"}}},"RouteHop":{"type":"object","description":"One destination of a trying step, in the order it would be tried.","required":["alias","answers"],"properties":{"alias":{"type":"string"},"answers":{"type":"boolean"},"status":{"type":"integer"},"ms":{"type":"integer","format":"int64"},"note":{"type":"string"},"reading":{"type":"string","description":"Why it is in this place, on a step that ranks by measurement or size."}}},"OrgModel":{"type":"object","description":"One of an organisation's models. Clients name only its alias.","required":["alias","org_id","kind","backends","backend_model","input_micros_per_mtok","output_micros_per_mtok","cached_input_micros_per_mtok","cache_write_micros_per_mtok","max_context_tokens","location","enabled"],"properties":{"alias":{"type":"string"},"org_id":{"type":"string"},"kind":{"type":"string","enum":["chat","completion","embedding","decision"]},"backends":{"type":["array","null"],"description":"Base URLs. `null` for anyone but an administrator.","items":{"type":"string"}},"backend_model":{"type":"string"},"provider":{"type":"string"},"display_name":{"type":"string"},"description":{"type":"string"},"input_micros_per_mtok":{"type":"integer","format":"int64"},"output_micros_per_mtok":{"type":"integer","format":"int64"},"cached_input_micros_per_mtok":{"type":"integer","format":"int64"},"cache_write_micros_per_mtok":{"type":"integer","format":"int64"},"long_prompt":{"$ref":"#/components/schemas/PriceTier"},"max_context_tokens":{"type":"integer"},"release_date":{"type":"string","format":"date"},"location":{"type":"string","description":"Where it runs, such as `ch`, `usa` or `onprem`."},"has_api_key":{"type":"boolean","description":"A credential is stored. Only administrators see it."},"subscription":{"type":"boolean","description":"Paid by each caller's own Claude plan."},"platform_key":{"type":"boolean","description":"The deployment holds the provider's key, and bills the model."},"enabled":{"type":"boolean"}}},"PriceTier":{"type":"object","description":"The prices for a prompt longer than `above_tokens`.","required":["above_tokens","input_micros_per_mtok","output_micros_per_mtok","cached_input_micros_per_mtok","cache_write_micros_per_mtok"],"properties":{"above_tokens":{"type":"integer","minimum":1,"description":"The longest prompt still charged the model's own prices."},"input_micros_per_mtok":{"type":"integer","format":"int64"},"output_micros_per_mtok":{"type":"integer","format":"int64"},"cached_input_micros_per_mtok":{"type":"integer","format":"int64"},"cache_write_micros_per_mtok":{"type":"integer","format":"int64"}}},"ModelStat":{"type":"object","description":"How fast a model has been answering.","required":["ttft_median_ms","ttft_p95_ms"],"properties":{"ttft_median_ms":{"type":"integer","format":"int64"},"ttft_p95_ms":{"type":"integer","format":"int64"}}},"ModelProbe":{"type":"object","description":"What one model check found.","required":["alias","reachable","streamed","tool_calls","tool_call_as_text","ok"],"properties":{"alias":{"type":"string"},"backend":{"type":"string","description":"The base URL that answered."},"reachable":{"type":"boolean"},"status":{"type":"integer"},"streamed":{"type":"boolean","description":"The answer arrived as server-sent events."},"tool_calls":{"type":"boolean","description":"The model's tool call came back in `tool_calls`."},"tool_call_as_text":{"type":"boolean","description":"The tool call came back as prose, a parser mismatch."},"tools_off":{"type":"boolean","description":"The backend refuses any request that offers tools."},"served_model":{"type":"string"},"ttft_ms":{"type":"integer","format":"int64"},"total_ms":{"type":"integer","format":"int64"},"sample":{"type":"string","description":"The start of the prose that came back."},"error":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"}},"no_credit":{"type":"boolean","description":"On the deployment's key with no credit left, so not called."},"ok":{"type":"boolean"}}},"HostedProvider":{"type":"object","description":"A hosted provider this build knows.","required":["name","endpoint","cached_input","location","kinds","models","summary"],"properties":{"name":{"type":"string"},"endpoint":{"type":"string","description":"The OpenAI-compatible base URL, or a template holding\n`{product_id}`. Empty for anyone but an administrator.\n"},"needs_product_id":{"type":"boolean","description":"Each customer has their own endpoint."},"cached_input":{"type":"boolean","description":"It publishes a cheaper rate for cached input."},"location":{"type":"string"},"kinds":{"type":"array","items":{"type":"string","enum":["chat","completion","embedding","decision"]}},"models":{"type":"array","items":{"$ref":"#/components/schemas/HostedProviderModel"}},"summary":{"type":"string"},"platform_key":{"type":"boolean","description":"The deployment holds this provider's key, so models take none."}}},"HostedProviderModel":{"type":"object","description":"One model a hosted provider serves.","required":["id","display_name","max_context_tokens","input_micros_per_mtok","output_micros_per_mtok","cached_input_micros_per_mtok","description"],"properties":{"id":{"type":"string"},"display_name":{"type":"string"},"max_context_tokens":{"type":"integer"},"release_date":{"type":"string","format":"date"},"input_micros_per_mtok":{"type":"integer","format":"int64"},"output_micros_per_mtok":{"type":"integer","format":"int64"},"cached_input_micros_per_mtok":{"type":"integer","format":"int64"},"cache_write_micros_per_mtok":{"type":"integer","format":"int64"},"long_prompt":{"$ref":"#/components/schemas/PriceTier"},"description":{"type":"string"},"snapshot":{"type":"string","description":"The dated id the maker also uses, such as `claude-haiku-4-5-20251001`."}}},"MCPServer":{"type":"object","description":"An MCP server the gateway stands in front of. See https://docs.keera.ch/manage/mcp/.","required":["org_id","alias","url","enabled"],"properties":{"org_id":{"type":"string"},"alias":{"type":"string"},"url":{"type":"string","description":"The Streamable HTTP endpoint. Empty for anyone but an administrator."},"description":{"type":"string"},"auth_header":{"type":"string","description":"The header the credential goes in. Empty means `Authorization`."},"has_api_key":{"type":"boolean","description":"A credential is stored."},"enabled":{"type":"boolean"}}},"ToolCallRow":{"type":"object","description":"One MCP tool call.","required":["id","created_at","server","tool","outcome","latency_ms","arg_bytes","result_bytes","cost_micros"],"properties":{"id":{"type":"integer","format":"int64"},"created_at":{"type":"string","format":"date-time"},"project_id":{"type":"string"},"user_id":{"type":"string"},"key_id":{"type":"string"},"server":{"type":"string","description":"The MCP server's alias."},"tool":{"type":"string"},"outcome":{"type":"string","enum":["ok","tool_error","error","denied","refused","input_required"]},"latency_ms":{"type":"integer"},"arg_bytes":{"type":"integer"},"result_bytes":{"type":"integer"},"cost_micros":{"type":"integer","format":"int64"},"error":{"type":"string"},"session_key":{"type":"string"},"client":{"type":"string"}}},"ToolSummary":{"type":"object","description":"One tool's calls in a window, added up.","required":["server","tool","calls","failed","denied","refused","latency_median_ms","arg_bytes","result_bytes"],"properties":{"server":{"type":"string"},"tool":{"type":"string"},"calls":{"type":"integer","format":"int64"},"failed":{"type":"integer","format":"int64","description":"The tool's own failures and calls that got no result."},"denied":{"type":"integer","format":"int64"},"refused":{"type":"integer","format":"int64"},"latency_median_ms":{"type":"integer","format":"int64"},"arg_bytes":{"type":"integer","format":"int64"},"result_bytes":{"type":"integer","format":"int64"}}},"ConnectClient":{"type":"object","description":"A coding agent that can be pointed at the gateway.","required":["client","label","about","lang","template","run"],"properties":{"client":{"type":"string","description":"Its id, as in `keera connect \u003cclient\u003e`."},"label":{"type":"string"},"about":{"type":"string"},"path":{"type":"string","description":"The file the block goes in. Empty for a command to run."},"lang":{"type":"string","enum":["json","toml","sh","python"]},"template":{"type":"string","description":"The block, with `{{base}}`, `{{models}}`, `{{aliases}}`,\n`{{alias}}` and sometimes `{{key}}` to fill in.\n"},"names":{"type":"object","description":"Other ids each Anthropic model goes by.","additionalProperties":{"type":"array","items":{"type":"string"}}},"entry":{"type":"string","description":"What `{{models}}` repeats once per model."},"run":{"type":"string","description":"What to type once it is configured."},"note":{"type":"string"}}},"ConnectModel":{"type":"object","description":"One model or router a client configuration names.","required":["alias"],"properties":{"alias":{"type":"string"},"name":{"type":"string","description":"Its name for people. Left out, one is made from the alias."},"max_context_tokens":{"type":"integer","description":"Left out for the default. A router has none."},"backend_model":{"type":"string"},"provider":{"type":"string"}}},"ConnectedClient":{"type":"object","description":"One client that called with the caller's keys.","required":["client","label","requests","last_used_at","keys"],"properties":{"client":{"type":"string"},"label":{"type":"string"},"requests":{"type":"integer","format":"int64"},"last_used_at":{"type":"string","format":"date-time"},"keys":{"type":"array","description":"Newest first.","items":{"$ref":"#/components/schemas/ConnectedClientKey"}}}},"ConnectedClientKey":{"type":"object","description":"One key a client called with.","required":["id","name","requests","last_used_at"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"requests":{"type":"integer","format":"int64"},"last_used_at":{"type":"string","format":"date-time"}}},"ReportLabels":{"type":"object","description":"The names a report's ids stand for. Every map holds the organisation's ids, not only those in the rows.","required":["project_names","key_names","user_names","user_full_names"],"properties":{"project_names":{"type":"object","additionalProperties":{"type":"string"},"description":"Project names by id."},"key_names":{"type":"object","additionalProperties":{"type":"string"},"description":"Key names by id."},"user_names":{"type":"object","additionalProperties":{"type":"string"},"description":"Email addresses by user id."},"user_full_names":{"type":"object","additionalProperties":{"type":"string"},"description":"Names by user id, for those who have one."}}},"DiagnosticCheck":{"type":"object","description":"One thing a deployment has or has not. Go type `Check`.","required":["area","name","verdict","detail"],"properties":{"area":{"type":"string","description":"Groups the checks, such as `Deployment`, `Models` or `Routers`."},"name":{"type":"string"},"verdict":{"type":"string","enum":["ok","warn","fail"],"description":"`warn` runs but lacks something; `fail` stops requests now."},"detail":{"type":"string","description":"What was found."},"fix":{"type":"string","description":"What to do about it. Every failing check has one."}}},"Diagnosis":{"type":"object","required":["checks","failures","warnings","probed"],"properties":{"checks":{"type":"array","items":{"$ref":"#/components/schemas/DiagnosticCheck"}},"failures":{"type":"integer"},"warnings":{"type":"integer"},"probed":{"type":"boolean","description":"Whether the models were actually called."}}},"SetupState":{"type":"object","description":"What a deployment already has. Go type `Setup`.","required":["orgs","keys","models","requests","filters","routers","sandboxes","mcp_servers","credits","invoiced"],"properties":{"orgs":{"type":"integer","format":"int64","description":"Organisations in the deployment, whichever is asked about."},"keys":{"type":"integer","format":"int64","description":"Active API keys."},"models":{"type":"integer","format":"int64","description":"Enabled models."},"requests":{"type":"integer","format":"int64","description":"1 once anything was served, else 0."},"filters":{"type":"integer","format":"int64"},"routers":{"type":"integer","format":"int64"},"sandboxes":{"type":"integer","format":"int64","description":"Sandbox classes."},"mcp_servers":{"type":"integer","format":"int64"},"credits":{"type":"integer","format":"int64","description":"Paid or granted payments that added credit."},"invoiced":{"type":"boolean","description":"Billed by invoice. Always false across every organisation."}}},"UsageBucket":{"type":"object","description":"One row of a usage report.","required":["group","requests","input_tokens","output_tokens","cost_micros","subscription_micros"],"properties":{"group":{"type":"string","description":"The id or value grouped by. Empty for requests without one."},"org_id":{"type":"string","description":"Only across every organisation, grouped by model."},"requests":{"type":"integer","format":"int64","description":"Requests served."},"input_tokens":{"type":"integer","format":"int64"},"output_tokens":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64","description":"Cost in millionths of a franc, refused and failed requests included."},"subscription_micros":{"type":"integer","format":"int64","description":"What Claude subscriptions paid for, at API prices. Not part of `cost_micros`."}}},"OverviewSeriesPoint":{"type":"object","description":"One bucket of the dashboard chart. Go type `SeriesPoint`.","required":["at","requests","tokens","cost_micros","failed"],"properties":{"at":{"type":"string","format":"date-time"},"requests":{"type":"integer","format":"int64"},"tokens":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"failed":{"type":"integer","format":"int64"}}},"Overview":{"type":"object","description":"The dashboard's figures.","required":["from","to","bucket","requests","input_tokens","output_tokens","cost_micros","subscription_micros","refused","failed","ttft_median_ms","ttft_p95_ms","series","top_projects","top_models"],"properties":{"from":{"type":"string","format":"date-time","description":"Widened to whole days over more than two days."},"to":{"type":"string","format":"date-time"},"bucket":{"type":"string","enum":["hour","day"],"description":"Hourly up to two days, daily beyond."},"requests":{"type":"integer","format":"int64"},"input_tokens":{"type":"integer","format":"int64"},"output_tokens":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"subscription_micros":{"type":"integer","format":"int64","description":"What Claude subscriptions paid for, at API prices. Not part of `cost_micros`."},"refused":{"type":"integer","format":"int64"},"failed":{"type":"integer","format":"int64"},"ttft_median_ms":{"type":"integer","format":"int64"},"ttft_p95_ms":{"type":"integer","format":"int64"},"series":{"type":"array","items":{"$ref":"#/components/schemas/OverviewSeriesPoint"}},"top_projects":{"type":"array","items":{"$ref":"#/components/schemas/UsageBucket"}},"top_models":{"type":"array","items":{"$ref":"#/components/schemas/UsageBucket"}},"top_keys":{"type":"array","items":{"$ref":"#/components/schemas/UsageBucket"},"description":"Only on a narrowed overview."}}},"UsageLine":{"type":"object","description":"One group's line on the usage chart.","required":["group","total","counts"],"properties":{"group":{"type":"string"},"org_id":{"type":"string","description":"Only across every organisation, grouped by model."},"other":{"type":"boolean","description":"This line sums every group without one of its own."},"total":{"type":"integer","format":"int64"},"counts":{"type":"array","items":{"type":"integer","format":"int64"},"description":"Served requests at each time in `at`."}}},"UsageChart":{"type":"object","description":"Served requests over time.","required":["bucket","at","lines"],"properties":{"bucket":{"type":"string","enum":["hour","day"]},"at":{"type":"array","items":{"type":"string","format":"date-time"}},"lines":{"type":"array","items":{"$ref":"#/components/schemas/UsageLine"},"description":"The five busiest groups, then the rest."}}},"MapClient":{"allOf":[{"type":"object","required":["client","label"],"properties":{"client":{"type":"string","description":"The client's key. Empty for senders that did not name themselves."},"label":{"type":"string","description":"How the client is shown."}}},{"$ref":"#/components/schemas/UsageCell"}],"description":"Something that called the gateway. Go type `mapClient`."},"MapModel":{"allOf":[{"type":"object","required":["alias","kind","enabled","hosting"],"properties":{"alias":{"type":"string"},"kind":{"type":"string","examples":["chat","completion","embedding","decision"]},"enabled":{"type":"boolean"},"hosting":{"type":"string","enum":["internal","external","unknown"],"description":"Which side of the customer's network the model runs on."},"endpoint":{"type":"string","description":"The host serving it. Administrators only."}}},{"$ref":"#/components/schemas/UsageCell"}],"description":"A model the gateway can forward to. Go type `mapModel`."},"MapFlow":{"allOf":[{"type":"object","required":["client","alias"],"properties":{"client":{"type":"string","description":"Empty for senders that did not name themselves."},"alias":{"type":"string"}}},{"$ref":"#/components/schemas/UsageCell"}],"description":"Traffic between one client and one model. Go type `store.Flow`."},"AuditEntry":{"type":"object","description":"One row of the audit log.","required":["id","created_at","actor","action"],"properties":{"id":{"type":"integer","format":"int64"},"created_at":{"type":"string","format":"date-time"},"actor":{"type":"string"},"action":{"type":"string","examples":["billing.topup"]},"target_type":{"type":"string"},"target_id":{"type":"string"},"detail":{"description":"Whatever the action recorded."}}},"AuditFacets":{"type":"object","description":"The actions and actors that occur in the log.","required":["actions","actors"],"properties":{"actions":{"type":"array","items":{"type":"string"}},"actors":{"type":"array","items":{"type":"string"}}}},"RequestSpan":{"type":"object","description":"One step of one request. Go type `store.Span`.","required":["name","at_us","for_us"],"properties":{"name":{"type":"string","description":"The step."},"of":{"type":"string","description":"What the step was about: a filter, a router or a model."},"at_us":{"type":"integer","format":"int64","description":"When it began, in microseconds after the gateway took the request."},"for_us":{"type":"integer","format":"int64","description":"How long it ran, in microseconds."},"note":{"type":"string","description":"The outcome in a few words."}}},"RequestLogEntry":{"type":"object","description":"One inference request. Go type `store.Request`.","required":["id","created_at","status","input_tokens","cached_input_tokens","cache_write_tokens","output_tokens","cost_micros","latency_ms","ttft_ms","stream","estimated","canceled"],"properties":{"id":{"type":"integer","format":"int64"},"created_at":{"type":"string","format":"date-time"},"alias":{"type":"string","description":"The model, as the client named it."},"status":{"type":"integer"},"error":{"type":"string","description":"What the client was told."},"note":{"type":"string","description":"What went wrong on the way to an answer served all the same."},"key_id":{"type":"string"},"project_id":{"type":"string"},"user_id":{"type":"string"},"org_id":{"type":"string"},"client":{"type":"string","description":"What sent the request, as it named itself."},"input_tokens":{"type":"integer","format":"int64"},"cached_input_tokens":{"type":"integer","format":"int64","description":"The part of the input served from the prompt cache."},"cache_write_tokens":{"type":"integer","format":"int64","description":"The part of the input written to the prompt cache."},"output_tokens":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"latency_ms":{"type":"integer","format":"int64"},"ttft_ms":{"type":"integer","format":"int64","description":"Wait for the first token."},"stream":{"type":"boolean"},"estimated":{"type":"boolean","description":"The token counts, and so the cost, are a guess: the client hung up first."},"canceled":{"type":"boolean"},"session_key":{"type":"string","description":"The conversation it was part of."},"spans":{"type":"array","items":{"$ref":"#/components/schemas/RequestSpan"},"description":"Where the time went."},"request_id":{"type":"string","description":"The id the gateway answered with."},"client_request_id":{"type":"string","description":"The client's own id."}}},"RequestOutcomes":{"type":"object","description":"A window counted by outcome.","required":["total","ok","failed","refused","interrupted"],"properties":{"total":{"type":"integer","format":"int64"},"ok":{"type":"integer","format":"int64"},"failed":{"type":"integer","format":"int64"},"refused":{"type":"integer","format":"int64"},"interrupted":{"type":"integer","format":"int64"}}},"FacetCount":{"type":"object","description":"One value a log can be narrowed by, and how many rows have it.","required":["value","count"],"properties":{"value":{"type":"string"},"count":{"type":"integer","format":"int64"}}},"RequestFacets":{"type":"object","description":"What a log can be narrowed by, over the whole window.","required":["models","keys","projects","users"],"properties":{"models":{"type":"array","items":{"$ref":"#/components/schemas/FacetCount"}},"keys":{"type":"array","items":{"$ref":"#/components/schemas/FacetCount"}},"projects":{"type":"array","items":{"$ref":"#/components/schemas/FacetCount"}},"users":{"type":"array","items":{"$ref":"#/components/schemas/FacetCount"}},"statuses":{"type":"array","items":{"$ref":"#/components/schemas/FacetCount"},"description":"On sessions, each session's last status."}}},"AgentSession":{"type":"object","description":"One task: a run of requests in one conversation.","required":["id","session_key","stated","started_at","ended_at","requests","ok","failed","refused","interrupted","input_tokens","output_tokens","cost_micros","ttft_median_ms","models","last_status"],"properties":{"id":{"type":"integer","format":"int64","description":"The id of its first request, which names it."},"session_key":{"type":"string","description":"The conversation."},"stated":{"type":"boolean","description":"The client named the session itself, rather than it being inferred."},"started_at":{"type":"string","format":"date-time"},"ended_at":{"type":"string","format":"date-time","description":"When the last request finished."},"requests":{"type":"integer","format":"int64"},"ok":{"type":"integer","format":"int64"},"failed":{"type":"integer","format":"int64"},"refused":{"type":"integer","format":"int64"},"interrupted":{"type":"integer","format":"int64"},"input_tokens":{"type":"integer","format":"int64"},"output_tokens":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"ttft_median_ms":{"type":"integer","format":"int64"},"models":{"type":"array","items":{"type":"string"},"description":"Every model it used."},"project_id":{"type":"string"},"user_id":{"type":"string"},"key_id":{"type":"string"},"last_status":{"type":"integer","description":"The status of its last request."},"last_error":{"type":"string","description":"The error of its last request, if any."}}},"AgentSessionTotals":{"type":"object","description":"What a window of sessions adds up to.","required":["sessions","unhappy","requests","input_tokens","output_tokens","cost_micros","requests_median","duration_median_ms","cost_median_micros","longest_requests","costliest_micros","outcomes"],"properties":{"sessions":{"type":"integer","format":"int64"},"unhappy":{"type":"integer","format":"int64","description":"Sessions holding something that did not deliver."},"requests":{"type":"integer","format":"int64"},"input_tokens":{"type":"integer","format":"int64"},"output_tokens":{"type":"integer","format":"int64"},"cost_micros":{"type":"integer","format":"int64"},"requests_median":{"type":"integer","format":"int64"},"duration_median_ms":{"type":"integer","format":"int64"},"cost_median_micros":{"type":"integer","format":"int64"},"longest_requests":{"type":"integer","format":"int64","description":"Requests in the longest session."},"costliest_micros":{"type":"integer","format":"int64","description":"Cost of the costliest session."},"outcomes":{"$ref":"#/components/schemas/RequestOutcomes","description":"Sessions by outcome, whichever outcome was asked for."}}},"BillingRow":{"type":"object","description":"One organisation's use of one provider model in a month.","required":["org_id","org_name","provider","backend_model","calls","input_tokens","cached_input_tokens","cache_write_tokens","output_tokens","billed_micros","credit_micros"],"properties":{"org_id":{"type":"string"},"org_name":{"type":"string","description":"Empty for an organisation deleted since."},"provider":{"type":"string"},"backend_model":{"type":"string","description":"The provider's name for the model."},"calls":{"type":"integer","format":"int64"},"input_tokens":{"type":"integer","format":"int64"},"cached_input_tokens":{"type":"integer","format":"int64"},"cache_write_tokens":{"type":"integer","format":"int64"},"output_tokens":{"type":"integer","format":"int64"},"billed_micros":{"type":"integer","format":"int64","description":"What the organisation is billed."},"credit_micros":{"type":"integer","format":"int64","description":"The part paid from credit."}}},"BillingTotal":{"type":"object","description":"The month's rows added up.","required":["calls","billed_micros"],"properties":{"calls":{"type":"integer","format":"int64"},"billed_micros":{"type":"integer","format":"int64"}}},"SavedCard":{"type":"object","required":["label"],"properties":{"label":{"type":"string","description":"How the card is shown."},"expires_at":{"type":"string","format":"date-time"}}},"CreditAccount":{"type":"object","description":"An organisation's credit for the deployment's provider keys, in CHF.","required":["org_id","balance_micros","topup_below_micros","topup_micros","invoiced"],"properties":{"org_id":{"type":"string"},"balance_micros":{"type":"integer","format":"int64","description":"Credit in millionths of a franc. Can go below 0."},"topup_below_micros":{"type":"integer","format":"int64","description":"Below this balance the saved card is charged. 0 is off."},"topup_micros":{"type":"integer","format":"int64","description":"What one automatic top-up pays. 0 is off."},"card":{"$ref":"#/components/schemas/SavedCard","description":"The saved card. Absent without one."},"topup_error":{"type":"string","description":"Why the last automatic top-up failed. None is tried until it is cleared."},"invoiced":{"type":"boolean","description":"Billed by invoice, so no credit is needed."}}},"Payment":{"type":"object","description":"Money into a credit account.","required":["id","org_id","created_at","kind","amount_micros","vat_micros","fee_micros","state","actor"],"properties":{"id":{"type":"string"},"org_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["topup","auto","grant"],"description":"A payment by card, an automatic top-up, or a grant."},"amount_micros":{"type":"integer","format":"int64","description":"The credit. Below 0 on a grant that takes some away."},"vat_micros":{"type":"integer","format":"int64"},"fee_micros":{"type":"integer","format":"int64","description":"The transaction fee on top. It is not credited."},"state":{"type":"string","enum":["pending","paid","failed"]},"save_card":{"type":"boolean"},"transaction_id":{"type":"integer","format":"int64","description":"PostFinance's transaction. Absent on a grant."},"actor":{"type":"string","description":"Who started it."},"note":{"type":"string","description":"Why a grant was made, or why a payment failed."},"settled_at":{"type":"string","format":"date-time"}}},"SandboxIsolation":{"type":"string","enum":["standard","isolated","vm"],"description":"`standard` is the usual container runtime, hardened; `isolated` puts gVisor in front of the host kernel; `vm` is a Kata Containers VM."},"SandboxPurpose":{"type":"string","enum":["engineer","agent"],"description":"`engineer` is a person's own machine, which people attach to and which is suspended when idle. `agent` is one task's machine, which nothing attaches to and which is terminated when its time runs out."},"SandboxState":{"type":"string","enum":["pending","ready","suspended","expired","failed","terminated"]},"SandboxClass":{"type":"object","description":"A machine a developer or an agent can ask for by name.","required":["org_id","name","image","isolation","cpu_millis","memory_mib","disk_mib","default_ttl_seconds","max_ttl_seconds","warm","created_at","updated_at"],"properties":{"org_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"image":{"type":"string","description":"The OCI image the sandbox runs."},"isolation":{"$ref":"#/components/schemas/SandboxIsolation"},"cpu_millis":{"type":"integer"},"memory_mib":{"type":"integer"},"disk_mib":{"type":"integer","description":"The persistent volume. Zero keeps nothing across a suspend."},"default_ttl_seconds":{"type":"integer"},"max_ttl_seconds":{"type":"integer"},"warm":{"type":"integer","description":"How many idle sandboxes are kept started."},"purposes":{"type":"array","items":{"$ref":"#/components/schemas/SandboxPurpose"},"description":"What it may be asked for. Absent means both."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Sandbox":{"type":"object","description":"One sandbox. The row outlives the machine.","required":["id","org_id","name","class","purpose","state","cpu_millis","memory_mib","disk_mib","created_at","running_seconds"],"properties":{"id":{"type":"string","examples":["sbx_..."]},"org_id":{"type":"string"},"project_id":{"type":"string"},"user_id":{"type":"string"},"owner":{"type":"string","description":"The address it was created for."},"name":{"type":"string"},"class":{"type":"string"},"purpose":{"$ref":"#/components/schemas/SandboxPurpose"},"state":{"$ref":"#/components/schemas/SandboxState"},"detail":{"type":"string","description":"Why it is in its state, such as why it is still pending."},"image":{"type":"string"},"isolation":{"$ref":"#/components/schemas/SandboxIsolation"},"cpu_millis":{"type":"integer"},"memory_mib":{"type":"integer"},"disk_mib":{"type":"integer"},"key_id":{"type":"string","description":"The API key minted for it."},"session_key":{"type":"string","description":"An agent sandbox's session, so the two can be shown together."},"repo":{"type":"string"},"branch":{"type":"string"},"node":{"type":"string"},"backing":{"type":"string","enum":["sandbox","claim"],"description":"A machine of its own, or a claim on a warm pool."},"created_at":{"type":"string","format":"date-time"},"ready_at":{"type":"string","format":"date-time"},"last_active_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"suspended_at":{"type":"string","format":"date-time"},"terminated_at":{"type":"string","format":"date-time"},"running_seconds":{"type":"integer","description":"How long it has actually held compute."}}},"SandboxUsage":{"type":"object","description":"What one group of sandboxes came to over a window.","required":["group","count","running_seconds","core_seconds","live"],"properties":{"group":{"type":"string","description":"A class name, project id or person's address."},"org_id":{"type":"string","description":"Only on a report across every organisation grouped by class."},"count":{"type":"integer"},"running_seconds":{"type":"integer"},"core_seconds":{"type":"integer","description":"Running seconds weighted by CPU."},"live":{"type":"integer","description":"How many still hold resources."}}},"InferenceModel":{"type":"object","description":"A model or router, with the fields of both OpenAI's and Anthropic's model objects.","required":["id","object","created","owned_by","type","display_name"],"properties":{"id":{"type":"string","description":"The alias to send as `model`."},"object":{"type":"string","const":"model"},"created":{"type":"integer","description":"The release date as a Unix time, or 0."},"owned_by":{"type":"string","const":"keera"},"type":{"type":"string","const":"model"},"created_at":{"type":"string","format":"date-time","description":"The release date, when one is stated."},"max_context_tokens":{"type":"integer"},"display_name":{"type":"string"},"description":{"type":"string"},"location":{"type":"string","description":"Where prompts sent to it go."},"destinations":{"type":"array","items":{"type":"string"},"description":"Only on routers: every model it may send to."}}},"InferenceModelList":{"type":"object","required":["object","data","has_more","first_id","last_id"],"properties":{"object":{"type":"string","const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/InferenceModel"}},"has_more":{"type":"boolean","const":false},"first_id":{"type":["string","null"]},"last_id":{"type":["string","null"]}}},"JsonRpcMessage":{"type":"object","description":"A JSON-RPC 2.0 request, notification or response.","required":["jsonrpc"],"additionalProperties":true,"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"type":["string","integer","null"]},"method":{"type":"string"},"params":{},"result":{},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{}}}}}},"responses":{"BadRequest":{"description":"The request is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"No valid credential was sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The caller may not do this.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"There is no such thing, or the caller may not see it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"It clashes with something that is already there.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"Over a limit. `Retry-After` says when to try again.","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Disabled":{"description":"This deployment has the feature turned off. The code ends in `_disabled`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PayloadTooLarge":{"description":"The body is over the size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}