> For the complete documentation index, see [llms.txt](https://docs.otari.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.otari.ai/api-reference/playground.md).

# Playground

## Playground Chat Completions

> Run one chat completion for the signed-in caller.\
> \
> Streaming and non-streaming both, identically to \`\`POST\
> /api/v1/chat/completions\`\`: this resolves the principal and then calls the\
> very same handler. The request is billed to the caller's own attribution\
> user in the workspace they named (or their organization's default), against\
> that user's budget, and writes the ordinary usage row with no \`\`api\_key\_id\`\`,\
> because there was no key.\
> \
> The body is \`\`ChatCompletionRequest\`\` unchanged, so the page sends the same\
> request an SDK would and a model, tool or parameter the gateway gains is\
> available here the day it lands. The workspace rides in the query string\
> rather than in the body for that reason: a field added to the body would\
> also have to be added to the pipeline's strip list, and a gateway-internal\
> field that is not stripped is forwarded to the provider as a call kwarg.\
> \
> \`\`user\`\` and \`\`safety\_identifier\`\` in the body are the fields the pipeline\
> will not read here: spend and the provider's end-user identifier both bind\
> to the session's own attribution user, derived and never accepted.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"ChatCompletionRequest":{"description":"OpenAI-compatible chat completion request.\n\nThe completion-param fields are derived from any-llm's ``CompletionParams``\n(see ``_schema_derive``) so the schema cannot silently drop a param any-llm\nforwards. Fields below either tighten a derived field (``messages``,\n``response_format``), declare an OpenAI wire param ``CompletionParams`` does\nnot model (``service_tier``, forwarded as an any-llm ``**kwargs`` param), add\ngateway-internal behavior (``mcp_servers``, ``mcp_server_ids``,\n``guardrails``, ``tools_header``, ``max_tool_iterations``) that is stripped\nbefore the request is forwarded upstream, or restate a derived field\nunchanged to document it (``max_completion_tokens``), which is only worth\ndoing where the wire contract is not guessable from the field itself.","properties":{"frequency_penalty":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Frequency Penalty"},"guardrails":{"anyOf":[{"items":{"$ref":"#/components/schemas/GuardrailConfig"},"maxItems":8,"type":"array"},{"type":"null"}],"title":"Guardrails"},"logit_bias":{"anyOf":[{"additionalProperties":{"type":"number"},"type":"object"},{"type":"null"}],"title":"Logit Bias"},"logprobs":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Logprobs"},"max_completion_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Upper bound on generated tokens. OpenAI's current name for the cap `max_tokens` used to carry; either field is accepted, and this one wins when a request sends both.","title":"Max Completion Tokens"},"max_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Tokens"},"max_tool_iterations":{"anyOf":[{"maximum":25,"minimum":1,"type":"integer"},{"type":"null"}],"title":"Max Tool Iterations"},"mcp_server_ids":{"anyOf":[{"items":{"format":"uuid","type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"title":"Mcp Server Ids"},"mcp_servers":{"anyOf":[{"items":{"$ref":"#/components/schemas/McpServerConfig"},"type":"array"},{"type":"null"}],"title":"Mcp Servers"},"messages":{"items":{"additionalProperties":true,"type":"object"},"minItems":1,"title":"Messages","type":"array"},"metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"description":"Tags for cost attribution, recorded on the request's usage rows and filterable in the usage API: up to 16 string pairs, keys up to 64 characters and values up to 512. A null value is ignored. LiteLLM's nested `spend_logs_metadata` object is also read, and wins over a flat key of the same name; it is never forwarded to the provider.","title":"Metadata"},"model":{"title":"Model","type":"string"},"n":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"N"},"parallel_tool_calls":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Parallel Tool Calls"},"presence_penalty":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Presence Penalty"},"prompt_cache_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt Cache Key"},"reasoning_effort":{"anyOf":[{"enum":["none","minimal","low","medium","high","xhigh","max","auto"],"type":"string"},{"type":"null"}],"default":"auto","title":"Reasoning Effort"},"response_format":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Response Format"},"safety_identifier":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A stable identifier for the end user behind the request, used as the tag the provider's end-user identifier is derived from. Wins over `user` for that, and is never sent upstream. The dashboard's Playground ignores it.","title":"Safety Identifier"},"seed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seed"},"service_tier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Service Tier"},"session_label":{"anyOf":[{"maxLength":255,"type":"string"},{"type":"null"}],"description":"Optional caller-supplied label for a run, experiment or conversation, which names the session the request's trace is grouped under. Stripped before the request is forwarded upstream to the provider.","title":"Session Label"},"stop":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Stop"},"stream":{"default":false,"title":"Stream","type":"boolean"},"stream_options":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Stream Options"},"temperature":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Temperature"},"tool_choice":{"anyOf":[{"type":"string"},{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Tool Choice"},"tools":{"anyOf":[{"items":{"anyOf":[{"additionalProperties":true,"type":"object"},{}]},"type":"array"},{"type":"null"}],"title":"Tools"},"tools_header":{"anyOf":[{"maxLength":4000,"type":"string"},{"type":"null"}],"description":"Optional override for the lead-in that the gateway prepends before the per-tool hint block in the system message. Useful for expressing global tool-selection policy (e.g. 'prefer MCP tools over code_execution'). Falls back to OTARI_TOOLS_HEADER env, then to the built-in default.","title":"Tools Header"},"top_logprobs":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Top Logprobs"},"top_p":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Top P"},"user":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User"}},"required":["model","messages"],"title":"ChatCompletionRequest","type":"object"},"GuardrailConfig":{"description":"A single guardrail check the caller wants the gateway to enforce.\n\nURL safety: when ``url`` is supplied it is validated by\n:func:`gateway.services.guardrails.run_input_guardrails` (not here at parse\ntime — the check does a DNS lookup that must be awaited) with the same\nSSRF guard used for MCP server URLs (loopback allowed by default for\nsame-host sidecars; gated by ``OTARI_MCP_ALLOW_LOOPBACK`` /\n``OTARI_MCP_ALLOW_PRIVATE_HOSTS``). Most deployments omit ``url`` and rely\non the operator-set ``OTARI_GUARDRAILS_URL`` instead.","properties":{"mode":{"default":"monitor","enum":["block","monitor"],"title":"Mode","type":"string"},"on":{"items":{"enum":["input","output"],"type":"string"},"title":"On","type":"array"},"on_unavailable":{"default":"block","enum":["block","monitor"],"title":"On Unavailable","type":"string"},"profile":{"maxLength":128,"minLength":1,"title":"Profile","type":"string"},"url":{"anyOf":[{"minLength":1,"type":"string"},{"type":"null"}],"title":"Url"},"validate_kwargs":{"additionalProperties":true,"title":"Validate Kwargs","type":"object"}},"required":["profile"],"title":"GuardrailConfig","type":"object"},"McpServerConfig":{"description":"Inline MCP server configuration accepted by generation requests.\n\nStreamable HTTP transport. The `url` must be reachable from the gateway process.\n\nURL safety (SSRF guard against private/link-local/reserved IP ranges, plus\nrejecting plain ``http://`` when ``authorization_token`` is set) is\nenforced by :func:`gateway.services.url_safety.validate_mcp_url`, called\nfrom the async request pipeline (``prepare_gateway_tools``) rather than\nhere at parse time: the safety check does a DNS lookup, which must be\nawaited so it can't block the event loop, and Pydantic validators run\nsynchronously during request-body parsing.","properties":{"allowed_tools":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed Tools"},"authorization_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization Token"},"name":{"maxLength":128,"minLength":1,"title":"Name","type":"string"},"purpose_hint":{"anyOf":[{"maxLength":2000,"type":"string"},{"type":"null"}],"title":"Purpose Hint"},"url":{"minLength":1,"title":"Url","type":"string"}},"required":["name","url"],"title":"McpServerConfig","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/chat/completions":{"post":{"description":"Run one chat completion for the signed-in caller.\n\nStreaming and non-streaming both, identically to ``POST\n/api/v1/chat/completions``: this resolves the principal and then calls the\nvery same handler. The request is billed to the caller's own attribution\nuser in the workspace they named (or their organization's default), against\nthat user's budget, and writes the ordinary usage row with no ``api_key_id``,\nbecause there was no key.\n\nThe body is ``ChatCompletionRequest`` unchanged, so the page sends the same\nrequest an SDK would and a model, tool or parameter the gateway gains is\navailable here the day it lands. The workspace rides in the query string\nrather than in the body for that reason: a field added to the body would\nalso have to be added to the pipeline's strip list, and a gateway-internal\nfield that is not stripped is forwarded to the provider as a call kwarg.\n\n``user`` and ``safety_identifier`` in the body are the fields the pipeline\nwill not read here: spend and the provider's end-user identifier both bind\nto the session's own attribution user, derived and never accepted.","operationId":"playground-playground_chat_completions","parameters":[{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCompletionRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Playground Chat Completions","tags":["playground"]}}}}
```

## List Playground Comparisons

> The caller's own rated comparisons in one workspace, newest first.\
> \
> Without the two answer bodies: the list shows a dozen rows and renders\
> neither, and there is no detail endpoint because the page has no screen that\
> reads one back. A comparison is a judgment that was recorded, not a\
> transcript to resume.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundComparisonsPublic":{"properties":{"data":{"items":{"$ref":"#/components/schemas/PlaygroundComparisonSummary"},"title":"Data","type":"array"}},"required":["data"],"title":"PlaygroundComparisonsPublic","type":"object"},"PlaygroundComparisonSummary":{"description":"A row in the comparison history: the question, the pair, the verdict.\n\nDeliberately without the two answers. The list shows a dozen rows at once\nand none of them renders an answer body, so sending them would move\nmegabytes to draw a few lines of text. There is no detail endpoint either,\nbecause the page has no screen that reads one back: a comparison is a\njudgment that was recorded, not a transcript to resume.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"format":"uuid","title":"Id","type":"string"},"model_a":{"title":"Model A","type":"string"},"model_b":{"title":"Model B","type":"string"},"preference":{"title":"Preference","type":"string"},"user_question":{"title":"User Question","type":"string"},"workspace_id":{"format":"uuid","title":"Workspace Id","type":"string"}},"required":["id","workspace_id","user_question","model_a","model_b","preference","created_at"],"title":"PlaygroundComparisonSummary","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/comparisons":{"get":{"description":"The caller's own rated comparisons in one workspace, newest first.\n\nWithout the two answer bodies: the list shows a dozen rows and renders\nneither, and there is no detail endpoint because the page has no screen that\nreads one back. A comparison is a judgment that was recorded, not a\ntranscript to resume.","operationId":"playground-list_playground_comparisons","parameters":[{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundComparisonsPublic"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Playground Comparisons","tags":["playground"]}}}}
```

## Save Playground Comparison

> Record which of two models answered a question better.\
> \
> 403 when comparison retention has not been granted; this is the flag with\
> the wider disclosure, because the row keeps both models' full answers.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundComparisonCreate":{"description":"One rated A/B exchange.\n\nBoth answers in full, which is the disclosure the comparison consent flag\ncovers: a preference with no answers attached is a datum nobody can later\ncheck, and the page's own history list shows the question and the two model\nids from these columns.","properties":{"model_a":{"maxLength":512,"title":"Model A","type":"string"},"model_a_answer":{"maxLength":200000,"title":"Model A Answer","type":"string"},"model_b":{"maxLength":512,"title":"Model B","type":"string"},"model_b_answer":{"maxLength":200000,"title":"Model B Answer","type":"string"},"preference":{"enum":["model_a","model_b","tie"],"title":"Preference","type":"string"},"user_question":{"maxLength":200000,"minLength":1,"title":"User Question","type":"string"},"workspace_id":{"format":"uuid","title":"Workspace Id","type":"string"}},"required":["workspace_id","user_question","model_a","model_b","model_a_answer","model_b_answer","preference"],"title":"PlaygroundComparisonCreate","type":"object"},"PlaygroundComparisonSummary":{"description":"A row in the comparison history: the question, the pair, the verdict.\n\nDeliberately without the two answers. The list shows a dozen rows at once\nand none of them renders an answer body, so sending them would move\nmegabytes to draw a few lines of text. There is no detail endpoint either,\nbecause the page has no screen that reads one back: a comparison is a\njudgment that was recorded, not a transcript to resume.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"format":"uuid","title":"Id","type":"string"},"model_a":{"title":"Model A","type":"string"},"model_b":{"title":"Model B","type":"string"},"preference":{"title":"Preference","type":"string"},"user_question":{"title":"User Question","type":"string"},"workspace_id":{"format":"uuid","title":"Workspace Id","type":"string"}},"required":["id","workspace_id","user_question","model_a","model_b","preference","created_at"],"title":"PlaygroundComparisonSummary","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/comparisons":{"post":{"description":"Record which of two models answered a question better.\n\n403 when comparison retention has not been granted; this is the flag with\nthe wider disclosure, because the row keeps both models' full answers.","operationId":"playground-save_playground_comparison","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundComparisonCreate"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundComparisonSummary"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Save Playground Comparison","tags":["playground"]}}}}
```

## Delete Playground Comparison

> Delete one of the caller's saved comparisons.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/comparisons/{comparison_id}":{"delete":{"description":"Delete one of the caller's saved comparisons.","operationId":"playground-delete_playground_comparison","parameters":[{"in":"path","name":"comparison_id","required":true,"schema":{"format":"uuid","title":"Comparison Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Playground Comparison","tags":["playground"]}}}}
```

## Read Playground Consent

> What the caller has agreed the Playground may store.\
> \
> Both flags false for a caller who has never answered, and nothing is\
> written: the page asks at the moment it needs the grant, so recording an\
> answer on a page load would record one nobody gave.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundConsentPublic":{"description":"What this identity has agreed the Playground may store.\n\nTwo flags rather than one, matching what the page asks for at the moment it\nasks: saving a transcript and recording a model preference are different\ndisclosures (the second stores *both* models' full answers), and the old\npage asked about each separately at the point of use. An identity with no\nstored row reads back as both false.","properties":{"store_comparisons":{"default":false,"title":"Store Comparisons","type":"boolean"},"store_conversations":{"default":false,"title":"Store Conversations","type":"boolean"}},"title":"PlaygroundConsentPublic","type":"object"}}},"paths":{"/api/v1/playground/consent":{"get":{"description":"What the caller has agreed the Playground may store.\n\nBoth flags false for a caller who has never answered, and nothing is\nwritten: the page asks at the moment it needs the grant, so recording an\nanswer on a page load would record one nobody gave.","operationId":"playground-read_playground_consent","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundConsentPublic"}}},"description":"Successful Response"}},"summary":"Read Playground Consent","tags":["playground"]}}}}
```

## Update Playground Consent

> Grant or withdraw content retention, one flag at a time.\
> \
> An omitted flag is left as it was. Withdrawing blocks new saves and deletes\
> nothing: what was stored with consent stays until its owner deletes it, which\
> is what keeps a withdrawal from being a destructive action nobody asked for.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundConsentUpdate":{"description":"A partial update: an omitted flag is left as it was.\n\nTri-state on purpose. The page grants one flag at a time, just in time, so a\nrequest that carried both would silently re-assert the other, which is the\nwrong direction for a consent record to move on its own.","properties":{"store_comparisons":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Store Comparisons"},"store_conversations":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Store Conversations"}},"title":"PlaygroundConsentUpdate","type":"object"},"PlaygroundConsentPublic":{"description":"What this identity has agreed the Playground may store.\n\nTwo flags rather than one, matching what the page asks for at the moment it\nasks: saving a transcript and recording a model preference are different\ndisclosures (the second stores *both* models' full answers), and the old\npage asked about each separately at the point of use. An identity with no\nstored row reads back as both false.","properties":{"store_comparisons":{"default":false,"title":"Store Comparisons","type":"boolean"},"store_conversations":{"default":false,"title":"Store Conversations","type":"boolean"}},"title":"PlaygroundConsentPublic","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/consent":{"put":{"description":"Grant or withdraw content retention, one flag at a time.\n\nAn omitted flag is left as it was. Withdrawing blocks new saves and deletes\nnothing: what was stored with consent stays until its owner deletes it, which\nis what keeps a withdrawal from being a destructive action nobody asked for.","operationId":"playground-update_playground_consent","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundConsentUpdate"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundConsentPublic"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Update Playground Consent","tags":["playground"]}}}}
```

## List Playground Conversations

> The caller's own saved transcripts in one workspace, newest first.\
> \
> Not gated on consent: withdrawing it stops new saves, so a transcript saved\
> while it was granted has to stay listable and deletable by its owner.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundConversationsPublic":{"properties":{"data":{"items":{"$ref":"#/components/schemas/PlaygroundConversationSummary"},"title":"Data","type":"array"}},"required":["data"],"title":"PlaygroundConversationsPublic","type":"object"},"PlaygroundConversationSummary":{"description":"A row in the history list: enough to recognize, not the transcript.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"format":"uuid","title":"Id","type":"string"},"message_count":{"title":"Message Count","type":"integer"},"model":{"title":"Model","type":"string"},"title":{"title":"Title","type":"string"},"workspace_id":{"format":"uuid","title":"Workspace Id","type":"string"}},"required":["id","workspace_id","title","model","message_count","created_at"],"title":"PlaygroundConversationSummary","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/conversations":{"get":{"description":"The caller's own saved transcripts in one workspace, newest first.\n\nNot gated on consent: withdrawing it stops new saves, so a transcript saved\nwhile it was granted has to stay listable and deletable by its owner.","operationId":"playground-list_playground_conversations","parameters":[{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundConversationsPublic"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Playground Conversations","tags":["playground"]}}}}
```

## Save Playground Conversation

> Save one transcript whole, for the caller, in a workspace they belong to.\
> \
> 403 when content retention has not been granted. The page asks first, so\
> reaching that is a client that skipped the prompt: a consent gate enforced\
> only in the browser is not a consent gate.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundConversationCreate":{"description":"A transcript to save, whole: there is no append-a-turn endpoint.\n\nThe page saves on an explicit click, with the conversation it currently\nshows, so the write is one row plus its turns and a resave is a new\nconversation rather than a mutation of the old one. That is also what keeps\nthe ordering column honest: ``position`` is assigned here, from the list's\nown order, and never negotiated with a client over several requests.","properties":{"messages":{"items":{"$ref":"#/components/schemas/PlaygroundMessageCreate"},"maxItems":400,"minItems":1,"title":"Messages","type":"array"},"model":{"maxLength":512,"title":"Model","type":"string"},"title":{"maxLength":200,"minLength":1,"title":"Title","type":"string"},"workspace_id":{"format":"uuid","title":"Workspace Id","type":"string"}},"required":["workspace_id","model","title","messages"],"title":"PlaygroundConversationCreate","type":"object"},"PlaygroundMessageCreate":{"description":"One turn in a transcript being saved.","properties":{"attachments":{"items":{"$ref":"#/components/schemas/PlaygroundAttachment"},"maxItems":10,"title":"Attachments","type":"array"},"content":{"maxLength":200000,"title":"Content","type":"string"},"reasoning":{"anyOf":[{"maxLength":200000,"type":"string"},{"type":"null"}],"title":"Reasoning"},"role":{"enum":["user","assistant"],"title":"Role","type":"string"}},"required":["role","content"],"title":"PlaygroundMessageCreate","type":"object"},"PlaygroundAttachment":{"description":"A file one turn sent, as the page draws its chip and sends it again.\n\nA record of the attachment rather than a reference the gateway keeps alive:\nthe file can be deleted after the save, and a resumed turn that sends it\nthen does not send its contents.","properties":{"bytes":{"minimum":0,"title":"Bytes","type":"integer"},"file_id":{"maxLength":64,"title":"File Id","type":"string"},"filename":{"maxLength":255,"title":"Filename","type":"string"}},"required":["file_id","filename","bytes"],"title":"PlaygroundAttachment","type":"object"},"PlaygroundConversationSummary":{"description":"A row in the history list: enough to recognize, not the transcript.","properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"id":{"format":"uuid","title":"Id","type":"string"},"message_count":{"title":"Message Count","type":"integer"},"model":{"title":"Model","type":"string"},"title":{"title":"Title","type":"string"},"workspace_id":{"format":"uuid","title":"Workspace Id","type":"string"}},"required":["id","workspace_id","title","model","message_count","created_at"],"title":"PlaygroundConversationSummary","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/conversations":{"post":{"description":"Save one transcript whole, for the caller, in a workspace they belong to.\n\n403 when content retention has not been granted. The page asks first, so\nreaching that is a client that skipped the prompt: a consent gate enforced\nonly in the browser is not a consent gate.","operationId":"playground-save_playground_conversation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundConversationCreate"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundConversationSummary"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Save Playground Conversation","tags":["playground"]}}}}
```

## Delete Playground Conversation

> Delete one of the caller's saved transcripts, and its turns with it.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/conversations/{conversation_id}":{"delete":{"description":"Delete one of the caller's saved transcripts, and its turns with it.","operationId":"playground-delete_playground_conversation","parameters":[{"in":"path","name":"conversation_id","required":true,"schema":{"format":"uuid","title":"Conversation Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Playground Conversation","tags":["playground"]}}}}
```

## Read Playground Conversation Messages

> One saved transcript's turns, in order.\
> \
> 404 for a transcript belonging to somebody else, the same answer an\
> unknown id gets: the owner predicate is in the query, so the two are\
> indistinguishable from here. An empty transcript is not a state a save can\
> produce (the request requires at least one turn), so no rows means no row\
> for this caller.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundMessagesPublic":{"properties":{"data":{"items":{"$ref":"#/components/schemas/PlaygroundMessagePublic"},"title":"Data","type":"array"}},"required":["data"],"title":"PlaygroundMessagesPublic","type":"object"},"PlaygroundMessagePublic":{"description":"One stored turn, in the order it was saved.\n\nNo usage figures, matching what the save accepts: tokens, cost and timing\ndescribe the request that ran rather than the conversation, and a resumed\ntranscript reporting an old request's latency as this session's would be\nlying. The billing record for that request is its ``usage_logs`` row.","properties":{"attachments":{"default":[],"items":{"$ref":"#/components/schemas/PlaygroundAttachment"},"title":"Attachments","type":"array"},"content":{"title":"Content","type":"string"},"reasoning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reasoning"},"role":{"title":"Role","type":"string"}},"required":["role","content"],"title":"PlaygroundMessagePublic","type":"object"},"PlaygroundAttachment":{"description":"A file one turn sent, as the page draws its chip and sends it again.\n\nA record of the attachment rather than a reference the gateway keeps alive:\nthe file can be deleted after the save, and a resumed turn that sends it\nthen does not send its contents.","properties":{"bytes":{"minimum":0,"title":"Bytes","type":"integer"},"file_id":{"maxLength":64,"title":"File Id","type":"string"},"filename":{"maxLength":255,"title":"Filename","type":"string"}},"required":["file_id","filename","bytes"],"title":"PlaygroundAttachment","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/conversations/{conversation_id}/messages":{"get":{"description":"One saved transcript's turns, in order.\n\n404 for a transcript belonging to somebody else, the same answer an\nunknown id gets: the owner predicate is in the query, so the two are\nindistinguishable from here. An empty transcript is not a state a save can\nproduce (the request requires at least one turn), so no rows means no row\nfor this caller.","operationId":"playground-read_playground_conversation_messages","parameters":[{"in":"path","name":"conversation_id","required":true,"schema":{"format":"uuid","title":"Conversation Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundMessagesPublic"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Read Playground Conversation Messages","tags":["playground"]}}}}
```

## Read Playground Favorite Models

> The caller's pinned model keys in one workspace, in pinned order.\
> \
> Stored rather than kept in the browser, so a pin follows the person to their\
> other devices; that is what the hosted original did and what makes the\
> Favorites group in every picker worth having.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundFavoriteModelsPublic":{"description":"The pin list, most recently pinned first.","properties":{"model_keys":{"items":{"type":"string"},"title":"Model Keys","type":"array"}},"required":["model_keys"],"title":"PlaygroundFavoriteModelsPublic","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/favorite-models":{"get":{"description":"The caller's pinned model keys in one workspace, in pinned order.\n\nStored rather than kept in the browser, so a pin follows the person to their\nother devices; that is what the hosted original did and what makes the\nFavorites group in every picker worth having.","operationId":"playground-read_playground_favorite_models","parameters":[{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundFavoriteModelsPublic"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Read Playground Favorite Models","tags":["playground"]}}}}
```

## Replace Playground Favorite Models

> Replace the caller's pin list for one workspace.\
> \
> A replace rather than a toggle, because the order is part of the value and\
> the client already holds the list it is rendering. Two tabs racing therefore\
> resolve to one of the two lists rather than to an interleaving neither of\
> them showed. Model keys are not validated against the catalog: a pinned model\
> that leaves the catalog simply stops appearing in the picker, and refusing\
> the write would make a stale pin unremovable.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundFavoriteModelsUpdate":{"description":"The whole pin list, replacing whatever was stored.\n\nA replace rather than a toggle endpoint, because the client already holds\nthe list it is rendering and the order is part of it (a newly pinned model\nleads). Two tabs racing therefore resolve to one of the two lists rather\nthan to an interleaving neither of them showed.","properties":{"model_keys":{"items":{"maxLength":512,"type":"string"},"maxItems":50,"title":"Model Keys","type":"array"}},"required":["model_keys"],"title":"PlaygroundFavoriteModelsUpdate","type":"object"},"PlaygroundFavoriteModelsPublic":{"description":"The pin list, most recently pinned first.","properties":{"model_keys":{"items":{"type":"string"},"title":"Model Keys","type":"array"}},"required":["model_keys"],"title":"PlaygroundFavoriteModelsPublic","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/favorite-models":{"put":{"description":"Replace the caller's pin list for one workspace.\n\nA replace rather than a toggle, because the order is part of the value and\nthe client already holds the list it is rendering. Two tabs racing therefore\nresolve to one of the two lists rather than to an interleaving neither of\nthem showed. Model keys are not validated against the catalog: a pinned model\nthat leaves the catalog simply stops appearing in the picker, and refusing\nthe write would make a stale pin unremovable.","operationId":"playground-replace_playground_favorite_models","parameters":[{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundFavoriteModelsUpdate"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundFavoriteModelsPublic"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Replace Playground Favorite Models","tags":["playground"]}}}}
```

## List Playground Files

> The caller's own files in one workspace, newest first, in OpenAI's list shape.\
> \
> Every file the caller owns there is listed, including one uploaded with an\
> API key of theirs in the same workspace, because a message here can attach it.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"OpenAIFileList":{"description":"A page of files in OpenAI's list shape, whose cursor is the last entry's ID.","properties":{"data":{"items":{"$ref":"#/components/schemas/OpenAIFileObject"},"title":"Data","type":"array"},"first_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Id"},"has_more":{"title":"Has More","type":"boolean"},"last_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Id"},"object":{"const":"list","default":"list","title":"Object","type":"string"}},"required":["data","has_more","first_id","last_id"],"title":"OpenAIFileList","type":"object"},"OpenAIFileObject":{"description":"One file in OpenAI's file object shape.","properties":{"bytes":{"title":"Bytes","type":"integer"},"created_at":{"title":"Created At","type":"integer"},"expires_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires At"},"filename":{"title":"Filename","type":"string"},"id":{"title":"Id","type":"string"},"object":{"const":"file","default":"file","title":"Object","type":"string"},"purpose":{"title":"Purpose","type":"string"}},"required":["id","bytes","created_at","expires_at","filename","purpose"],"title":"OpenAIFileObject","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/files":{"get":{"description":"The caller's own files in one workspace, newest first, in OpenAI's list shape.\n\nEvery file the caller owns there is listed, including one uploaded with an\nAPI key of theirs in the same workspace, because a message here can attach it.","operationId":"playground-list_playground_files","parameters":[{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}},{"in":"query","name":"limit","required":false,"schema":{"default":100,"maximum":1000,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"after","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"After"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIFileList"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Playground Files","tags":["playground"]}}}}
```

## Upload Playground File

> Upload a file for the caller, in a workspace they belong to.\
> \
> The row is owned by the same principal a Playground completion runs as, so a\
> \`\`file\_id\`\` returned here resolves in the caller's own messages and in nobody\
> else's. \`\`POST /api/v1/files\`\` takes an API key, which a dashboard session\
> does not hold; this is the session's way in.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"Body_playground-upload_playground_file":{"properties":{"file":{"contentMediaType":"application/octet-stream","title":"File","type":"string"}},"required":["file"],"title":"Body_playground-upload_playground_file","type":"object"},"OpenAIFileObject":{"description":"One file in OpenAI's file object shape.","properties":{"bytes":{"title":"Bytes","type":"integer"},"created_at":{"title":"Created At","type":"integer"},"expires_at":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expires At"},"filename":{"title":"Filename","type":"string"},"id":{"title":"Id","type":"string"},"object":{"const":"file","default":"file","title":"Object","type":"string"},"purpose":{"title":"Purpose","type":"string"}},"required":["id","bytes","created_at","expires_at","filename","purpose"],"title":"OpenAIFileObject","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/files":{"post":{"description":"Upload a file for the caller, in a workspace they belong to.\n\nThe row is owned by the same principal a Playground completion runs as, so a\n``file_id`` returned here resolves in the caller's own messages and in nobody\nelse's. ``POST /api/v1/files`` takes an API key, which a dashboard session\ndoes not hold; this is the session's way in.","operationId":"playground-upload_playground_file","parameters":[{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_playground-upload_playground_file"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIFileObject"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Upload Playground File","tags":["playground"]}}}}
```

## Delete Playground File

> Delete one of the caller's files. Another identity's answers 404, as a nonexistent one does.\
> \
> A saved transcript that attached the file keeps its record of the\
> attachment, but the file is gone, so sending that turn again does not send\
> its contents.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"OpenAIFileDeleted":{"description":"OpenAI's answer to a delete.","properties":{"deleted":{"const":true,"default":true,"title":"Deleted","type":"boolean"},"id":{"title":"Id","type":"string"},"object":{"const":"file","default":"file","title":"Object","type":"string"}},"required":["id"],"title":"OpenAIFileDeleted","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/files/{file_id}":{"delete":{"description":"Delete one of the caller's files. Another identity's answers 404, as a nonexistent one does.\n\nA saved transcript that attached the file keeps its record of the\nattachment, but the file is gone, so sending that turn again does not send\nits contents.","operationId":"playground-delete_playground_file","parameters":[{"in":"path","name":"file_id","required":true,"schema":{"title":"File Id","type":"string"}},{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenAIFileDeleted"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Playground File","tags":["playground"]}}}}
```

## Read Playground Tools

> The gateway-run tools the caller's workspace may attach to a message.\
> \
> One read rather than the three the dashboard would otherwise make (the\
> deployment's tool settings, the workspace's web-search row, its\
> code-execution row), because the answer is a composition of them in a fixed\
> direction: the deployment decides whether a tool exists and the workspace may\
> only narrow that. Composing it here is what keeps the menu from offering\
> something the request path would refuse.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"PlaygroundToolsResponse":{"description":"What the caller's workspace may attach to a Playground message.","properties":{"code_execution":{"$ref":"#/components/schemas/PlaygroundToolStatus"},"files":{"$ref":"#/components/schemas/PlaygroundToolStatus","description":"Whether a message may attach a file uploaded here."},"mcp_servers":{"items":{"$ref":"#/components/schemas/PlaygroundMcpServer"},"title":"Mcp Servers","type":"array"},"web_search":{"$ref":"#/components/schemas/PlaygroundToolStatus"}},"required":["web_search","code_execution","files","mcp_servers"],"title":"PlaygroundToolsResponse","type":"object"},"PlaygroundToolStatus":{"description":"Whether one gateway-run tool can be attached right now, and why not.\n\nThree states from two fields, which is what the composer's menu draws: a\ntool the deployment never configured is not offered, one the deployment\nconfigured and this workspace turned off is shown disabled with the reason,\nand an available one is a plain checkbox. A single boolean would collapse\nthe first two, which is how a checkbox comes to look attachable and then\nfail at request time (otari-ai#1419).","properties":{"configured":{"description":"Whether this deployment has a backend for the tool at all.","title":"Configured","type":"boolean"},"enabled":{"description":"Whether the caller's workspace may attach it.","title":"Enabled","type":"boolean"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Why it cannot be attached. Null when it can.","title":"Reason"}},"required":["configured","enabled"],"title":"PlaygroundToolStatus","type":"object"},"PlaygroundMcpServer":{"description":"One of the workspace's MCP servers, as the tools menu lists it.","properties":{"enabled":{"title":"Enabled","type":"boolean"},"id":{"format":"uuid","title":"Id","type":"string"},"name":{"title":"Name","type":"string"},"purpose_hint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purpose Hint"}},"required":["id","name","enabled"],"title":"PlaygroundMcpServer","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/playground/tools":{"get":{"description":"The gateway-run tools the caller's workspace may attach to a message.\n\nOne read rather than the three the dashboard would otherwise make (the\ndeployment's tool settings, the workspace's web-search row, its\ncode-execution row), because the answer is a composition of them in a fixed\ndirection: the deployment decides whether a tool exists and the workspace may\nonly narrow that. Composing it here is what keeps the menu from offering\nsomething the request path would refuse.","operationId":"playground-read_playground_tools","parameters":[{"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Workspace to act in. Defaults to the caller's organization's default workspace. A workspace the caller is not a member of answers 404, as a nonexistent one does.","title":"Workspace Id"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaygroundToolsResponse"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Read Playground Tools","tags":["playground"]}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.otari.ai/api-reference/playground.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
