From 65de0dbe983c664c6ebc29e8161e9a8047520acf Mon Sep 17 00:00:00 2001 From: pamaljayasinghe Date: Wed, 27 May 2026 09:17:25 +0530 Subject: [PATCH 01/12] Multi Provider Routing Policy changes --- .../openai-header-router.yaml | 78 +++ .../default-policies/openai-to-anthropic.yaml | 84 +++ .../openai-to-azure-openai.yaml | 78 +++ .../default-policies/openai-to-mistral.yaml | 61 ++ gateway/build-manifest.yaml | 15 + gateway/build.yaml | 10 + gateway/examples/anthropic-openai-proxy.yaml | 42 ++ gateway/examples/anthropic-provider.yaml | 62 ++ gateway/examples/azure-openai-provider.yaml | 67 +++ gateway/examples/azure-openai-proxy.yaml | 37 ++ gateway/examples/gemini-provider.yaml | 58 ++ gateway/examples/llm-provider.yaml | 38 ++ gateway/examples/llm-proxy.yaml | 4 + gateway/examples/mistral-openai-proxy.yaml | 43 ++ gateway/examples/mistral-provider.yaml | 63 +++ .../examples/openai-multi-provider-proxy.yaml | 151 +++++ gateway/examples/openai-provider.yaml | 67 +++ .../api/management-openapi.yaml | 35 ++ .../pkg/api/management/generated.go | 528 +++++++++--------- .../pkg/config/llm_validator.go | 75 +++ .../pkg/utils/llm_transformer.go | 134 ++++- .../pkg/utils/llm_transformer_test.go | 128 +++++ .../slugify-body/policy-definition.yaml | 19 +- .../analytics/policy-definition.yaml | 7 +- .../api/v1alpha1/llmproxy_types.go | 22 + .../api/v1alpha1/zz_generated.deepcopy.go | 32 ++ ...eway.api-platform.wso2.com_llmproxies.yaml | 79 +++ .../controller/llmproxy_controller.go | 38 +- .../controller/llmproxy_controller_test.go | 82 +++ .../management_upstream_auth_payload.go | 20 + .../management_valuefrom_enqueue.go | 9 +- .../management_valuefrom_fingerprint.go | 6 +- ...eway.api-platform.wso2.com_llmproxies.yaml | 79 +++ mistral-proxy.yaml | 246 ++++++++ platform-api/src/api/generated.go | 12 + .../src/internal/dto/llm_deployment.go | 18 +- platform-api/src/internal/model/llm.go | 26 +- platform-api/src/internal/service/llm.go | 59 +- .../src/internal/service/llm_deployment.go | 17 +- platform-api/src/resources/openapi.yaml | 31 + 40 files changed, 2317 insertions(+), 343 deletions(-) create mode 100644 event-gateway/default-policies/openai-header-router.yaml create mode 100644 event-gateway/default-policies/openai-to-anthropic.yaml create mode 100644 event-gateway/default-policies/openai-to-azure-openai.yaml create mode 100644 event-gateway/default-policies/openai-to-mistral.yaml create mode 100644 gateway/examples/anthropic-openai-proxy.yaml create mode 100644 gateway/examples/anthropic-provider.yaml create mode 100644 gateway/examples/azure-openai-provider.yaml create mode 100644 gateway/examples/azure-openai-proxy.yaml create mode 100644 gateway/examples/gemini-provider.yaml create mode 100644 gateway/examples/mistral-openai-proxy.yaml create mode 100644 gateway/examples/mistral-provider.yaml create mode 100644 gateway/examples/openai-multi-provider-proxy.yaml create mode 100644 gateway/examples/openai-provider.yaml create mode 100644 mistral-proxy.yaml diff --git a/event-gateway/default-policies/openai-header-router.yaml b/event-gateway/default-policies/openai-header-router.yaml new file mode 100644 index 0000000000..c7633e3e3e --- /dev/null +++ b/event-gateway/default-policies/openai-header-router.yaml @@ -0,0 +1,78 @@ +name: openai-header-router +version: v1.0.0 +description: | + Selects a target LLM provider for each incoming OpenAI Chat Completions + request by inspecting a request header, then publishes the chosen + provider id into SharedContext.Metadata["selected_provider"]. + + This policy is a "setter" — its only job is to publish the selection. + Downstream consumer policies (the per-provider translators such as + openai-to-anthropic, openai-to-azure-openai, and openai-to-mistral) + read that key and own both the translation and the upstream-cluster + selection. + + Selection algorithm: + + 1. The configured header is read from the request (case-insensitive). + 2. Each entry in "mappings" is checked: if its "headerValue" matches the + incoming header value (case-insensitive), the entry's "provider" is + selected. + 3. If the header is absent, empty, or no mapping matches, the configured + "defaultProvider" is selected. + + The defaultProvider field is required so this policy always produces a + selection — there is no "pass through with no provider" mode. + +parameters: + type: object + additionalProperties: false + properties: + headerName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the request header to read for provider selection. + Comparison is case-insensitive. + default: x-provider + minLength: 1 + defaultProvider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id selected when the header is missing, empty, or does + not match any entry in "mappings". This must be a provider id that + a downstream translator is bound to. + minLength: 1 + mappings: + type: array + x-wso2-policy-advanced-param: false + description: | + Mapping from incoming header values to provider ids. The first + matching entry wins. + items: + type: object + additionalProperties: false + properties: + headerValue: + type: string + description: | + Header value to match (case-insensitive, leading/trailing + whitespace trimmed). + minLength: 1 + provider: + type: string + description: | + Provider id published to SharedContext.Metadata + ["selected_provider"] when this entry matches. + minLength: 1 + required: + - headerValue + - provider + minItems: 1 + required: + - mappings + - defaultProvider + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-anthropic.yaml b/event-gateway/default-policies/openai-to-anthropic.yaml new file mode 100644 index 0000000000..bd695e8ce0 --- /dev/null +++ b/event-gateway/default-policies/openai-to-anthropic.yaml @@ -0,0 +1,84 @@ +name: openai-to-anthropic +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the Anthropic + Messages API format and forwards it to an Anthropic-compatible upstream. + + Request transformations: + - system/developer messages are lifted into the top-level "system" field. + - messages[] is converted to Anthropic role/content blocks. + - assistant tool_calls become tool_use content blocks; tool-role messages + are grouped into a single user message of tool_result blocks. + - tools[] / tool_choice are converted to the Anthropic equivalents. + - stop is mapped to stop_sequences; image_url parts to image blocks. + - The request path is rewritten to /v1/messages and the anthropic-version + header is set. + + Response transformations: + - The Anthropic Messages response body is rewritten into the OpenAI + ChatCompletion shape (id, object, created, model, choices, usage). + - text content blocks become choice.message.content; tool_use blocks + become choice.message.tool_calls; stop_reason is mapped to + finish_reason; cache token counts are carried in usage + .prompt_tokens_details. + - Anthropic error envelopes are rewritten into OpenAI error envelopes + so clients see consistent error shapes regardless of provider. + - Streaming (Server-Sent Events) responses are passed through + untouched in this policy version; clients that need true streaming + should request non-streaming (omit "stream": true) until a streaming + variant ships. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests pass + through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Anthropic API key must be configured on the + upstream LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Anthropic model name used in the translated request (for example + claude-sonnet-4-20250514). Overrides the OpenAI "model" field. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + anthropicVersion: + type: string + x-wso2-policy-advanced-param: true + description: | + Value of the anthropic-version request header sent upstream. + default: "2023-06-01" + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-azure-openai.yaml b/event-gateway/default-policies/openai-to-azure-openai.yaml new file mode 100644 index 0000000000..67bb75e537 --- /dev/null +++ b/event-gateway/default-policies/openai-to-azure-openai.yaml @@ -0,0 +1,78 @@ +name: openai-to-azure-openai +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the Azure + OpenAI request shape and forwards it to an Azure-OpenAI-compatible + upstream. + + Transformation: + - The request path is rewritten to + /openai/deployments/{deployment}/{pathSuffix}?api-version=. + - The body passes through unchanged — Azure OpenAI accepts the OpenAI + request shape directly. + - The deployment id is taken from the "model" parameter when set, + otherwise from the request body's "model" field. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests + pass through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Azure api-key must be configured on the upstream + LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + apiVersion: + type: string + x-wso2-policy-advanced-param: false + description: | + Azure OpenAI api-version query-string value (for example + 2024-02-15-preview). Required because Azure rejects requests + without it. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Azure deployment id used in the rewritten path. When omitted, the + request body's "model" field is used as the deployment id. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + pathSuffix: + type: string + x-wso2-policy-advanced-param: true + description: | + Endpoint suffix appended after the deployment segment in the + rewritten path. Defaults to /chat/completions. + default: /chat/completions + minLength: 1 + required: + - apiVersion + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-mistral.yaml b/event-gateway/default-policies/openai-to-mistral.yaml new file mode 100644 index 0000000000..f2ac1d85bd --- /dev/null +++ b/event-gateway/default-policies/openai-to-mistral.yaml @@ -0,0 +1,61 @@ +name: openai-to-mistral +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the format + expected by Mistral AI's chat completions endpoint and forwards it to a + Mistral-compatible upstream. + + Mistral's chat completions API is largely OpenAI-compatible, so the + translation is small in scope: + - The "model" field is overridden with the configured Mistral model. + - OpenAI request fields that Mistral does not accept (logprobs, n, + service_tier, store, metadata, user, etc.) are stripped. + - The request path is rewritten to /v1/chat/completions. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests + pass through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Mistral API key must be configured on the upstream + LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Mistral model name used in the translated request (for example + mistral-large-latest). Overrides the OpenAI "model" field. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/gateway/build-manifest.yaml b/gateway/build-manifest.yaml index bb2df6447e..11408fcd27 100644 --- a/gateway/build-manifest.yaml +++ b/gateway/build-manifest.yaml @@ -123,3 +123,18 @@ policies: - name: word-count-guardrail version: v1.0.2 gomodule: github.com/wso2/gateway-controllers/policies/word-count-guardrail@v1 + - name: openai-to-azure-openai + version: v1.0.0 + filePath: ./dev-policies/openai-to-azure-openai + - name: openai-to-anthropic + version: v1.0.0 + filePath: ./dev-policies/openai-to-anthropic + - name: openai-to-mistral + version: v1.0.0 + filePath: ./dev-policies/openai-to-mistral + - name: openai-to-gemini + version: v1.0.0 + filePath: ./dev-policies/openai-to-gemini + - name: openai-header-router + version: v1.0.0 + filePath: ./dev-policies/openai-header-router diff --git a/gateway/build.yaml b/gateway/build.yaml index e0bada193c..407a542743 100644 --- a/gateway/build.yaml +++ b/gateway/build.yaml @@ -84,3 +84,13 @@ policies: gomodule: github.com/wso2/gateway-controllers/policies/url-guardrail@v1 - name: word-count-guardrail gomodule: github.com/wso2/gateway-controllers/policies/word-count-guardrail@v1 + - name: openai-to-azure-openai + filePath: ./dev-policies/openai-to-azure-openai + - name: openai-to-anthropic + filePath: ./dev-policies/openai-to-anthropic + - name: openai-to-mistral + filePath: ./dev-policies/openai-to-mistral + - name: openai-to-gemini + filePath: ./dev-policies/openai-to-gemini + - name: openai-header-router + filePath: ./dev-policies/openai-header-router diff --git a/gateway/examples/anthropic-openai-proxy.yaml b/gateway/examples/anthropic-openai-proxy.yaml new file mode 100644 index 0000000000..4ac65baec2 --- /dev/null +++ b/gateway/examples/anthropic-openai-proxy.yaml @@ -0,0 +1,42 @@ +# -------------------------------------------------------------------- +# OpenAI -> Anthropic LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-anthropic` policy translates into Anthropic's Messages API +# format and forwards to the existing `anthropic-provider` upstream +# (api.anthropic.com). +# +# Single-provider mode: there is no router in front of the policy, so +# nothing sets metadata["selected_provider"] and the translator runs on +# every request. "id" is omitted, so it routes to the default upstream +# (the primary provider above). +# +# The provider must already be deployed and must have its x-api-key +# set on upstream.auth (the policy does NOT inject the Anthropic key). +# +# The provider example's api-key-auth policy expects the proxy to send the +# issued provider loopback key in X-API-Key. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-anthropic +spec: + displayName: OpenAI to Anthropic Proxy + version: v1.0 + context: /openai-anthropic + provider: + id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + policies: + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 diff --git a/gateway/examples/anthropic-provider.yaml b/gateway/examples/anthropic-provider.yaml new file mode 100644 index 0000000000..6e1e8ec75b --- /dev/null +++ b/gateway/examples/anthropic-provider.yaml @@ -0,0 +1,62 @@ +# -------------------------------------------------------------------- +# Anthropic LLM Provider Configuration +# +# Uses the built-in `anthropic` template and points to the public +# Anthropic API endpoint. Replace the placeholder with a real Anthropic +# API key (sk-ant-...) before deploying. +# +# The openai-to-anthropic policy on the proxy layer translates +# OpenAI-format requests into Anthropic's Messages API wire format +# (path /v1/messages, anthropic-version header, body rewrite). +# +# Auth: upstream.auth is the gateway -> Anthropic vendor auth. Proxies in +# front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: anthropic-provider +spec: + displayName: Anthropic Provider + version: v1.0 + template: anthropic + upstream: + url: https://api.anthropic.com + auth: + type: api-key + header: x-api-key + # Anthropic's header is x-api-key with the raw key (NO "Bearer" prefix). + value: sk-ant-REPLACE_WITH_ANTHROPIC_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/messages + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/messages + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: anthropic-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: anthropic-provider + displayName: Anthropic provider loopback key + apiKey: + value: anthropic_provider_loopback_key_1234567890abcdef + # anthropic_provider_loopback_key_1234567890abcdef + maskedApiKey: anthropic_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/azure-openai-provider.yaml b/gateway/examples/azure-openai-provider.yaml new file mode 100644 index 0000000000..e1345ac3c4 --- /dev/null +++ b/gateway/examples/azure-openai-provider.yaml @@ -0,0 +1,67 @@ +# -------------------------------------------------------------------- +# Azure OpenAI LLM Provider Configuration +# +# Uses the built-in 'azure-openai' template and points to your Azure +# OpenAI endpoint. The openai-to-azure-openai policy on the proxy layer +# translates OpenAI-format requests into the Azure OpenAI wire format +# (path rewriting, api-version query param). +# +# Replace before deploying: +# - url: your Azure OpenAI endpoint, e.g. https://my-resource.openai.azure.com +# - value: your Azure OpenAI API key +# +# Auth: upstream.auth is the gateway -> Azure vendor auth (header: api-key). +# Proxies in front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: azure-openai-provider +spec: + displayName: Azure OpenAI Provider + version: v1.0 + template: azure-openai + upstream: + url: #endpoint url + auth: + type: api-key + header: api-key + value: testkeyaddhere + policies: + - name: api-key-auth + version: v1 + paths: + - path: /openai/deployments/{deployment}/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /openai/deployments/{deployment}/chat/completions + methods: [POST] + - path: /openai/deployments/{deployment}/completions + methods: [POST] + - path: /openai/deployments/{deployment}/embeddings + methods: [POST] + - path: /openai/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: azure-openai-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: azure-openai-provider + displayName: Azure OpenAI provider loopback key + apiKey: + value: azure_openai_provider_loopback_key_1234567890abcdef + maskedApiKey: azure_openai_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/azure-openai-proxy.yaml b/gateway/examples/azure-openai-proxy.yaml new file mode 100644 index 0000000000..0466a62e72 --- /dev/null +++ b/gateway/examples/azure-openai-proxy.yaml @@ -0,0 +1,37 @@ +# -------------------------------------------------------------------- +# OpenAI -> Azure OpenAI LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-azure-openai` policy translates into Azure OpenAI's +# /openai/deployments/{deployment}/chat/completions?api-version=... +# format and forwards to the `azure-openai-provider` upstream. +# +# Single-provider mode: the policy omits the "provider" parameter, so +# it runs on every request without needing a setter policy. +# +# Replace before deploying: +# - apiVersion param: a valid Azure OpenAI api-version +# (e.g. 2024-02-15-preview) +# - model param (optional): pin the deployment id here, otherwise +# the request body's "model" field is used as the deployment. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-azure-openai +spec: + displayName: OpenAI to Azure OpenAI Proxy + version: v1.0 + context: /azure-openai + provider: + id: azure-openai-provider + policies: + - name: openai-to-azure-openai + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + apiVersion: "2024-02-15-preview" + model: gpt-4o diff --git a/gateway/examples/gemini-provider.yaml b/gateway/examples/gemini-provider.yaml new file mode 100644 index 0000000000..833bc962e5 --- /dev/null +++ b/gateway/examples/gemini-provider.yaml @@ -0,0 +1,58 @@ +# -------------------------------------------------------------------- +# Gemini LLM Provider Configuration +# +# Uses the built-in `gemini` template and points to Google's Gemini API. +# The openai-to-gemini policy on the proxy layer translates OpenAI-format +# requests into Gemini's generateContent API format. +# +# Auth: upstream.auth is the gateway -> Gemini vendor auth +# (header: x-goog-api-key). Proxies inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: gemini-provider +spec: + displayName: Gemini Provider + version: v1.0 + template: gemini + upstream: + url: https://generativelanguage.googleapis.com + auth: + type: api-key + header: x-goog-api-key + value: REPLACE_WITH_GEMINI_API_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1beta/models/{model}:generateContent + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1beta/models/{model}:generateContent + methods: [POST] + - path: /v1beta/models/{model}:streamGenerateContent + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: gemini-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: gemini-provider + displayName: Gemini provider loopback key + apiKey: + value: gemini_provider_loopback_key_1234567890abcdef + maskedApiKey: gemini_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/llm-provider.yaml b/gateway/examples/llm-provider.yaml index 7b4b8a7686..4585091617 100644 --- a/gateway/examples/llm-provider.yaml +++ b/gateway/examples/llm-provider.yaml @@ -31,6 +31,25 @@ spec: type: api-key header: Authorization value: api_key_abc123 + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - path: /models + methods: [GET] + params: + key: X-API-Key + in: header + - path: /models/{modelId} + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: @@ -40,3 +59,22 @@ spec: methods: [GET] - path: /models/{modelId} methods: [GET] + +--- +# Provider API key used by clients or by an LlmProxy provider.auth block. +# In production, prefer apiKey.valueFrom with a Kubernetes Secret. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: my-llm-provider-key +spec: + parentRef: + kind: LlmProvider + name: my-llm-provider + displayName: My LLM provider key + apiKey: + value: provider_loopback_key_1234567890abcdef1234567890 + maskedApiKey: provider_loopback_key_1234********7890 + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/llm-proxy.yaml b/gateway/examples/llm-proxy.yaml index fa3b6d5c02..5a78844a8c 100644 --- a/gateway/examples/llm-proxy.yaml +++ b/gateway/examples/llm-proxy.yaml @@ -26,6 +26,10 @@ spec: context: /assistant provider: id: my-llm-provider + auth: + type: api-key + header: X-API-Key + value: provider_loopback_key_1234567890abcdef1234567890 policies: - name: semanticPromptGuard version: v1 diff --git a/gateway/examples/mistral-openai-proxy.yaml b/gateway/examples/mistral-openai-proxy.yaml new file mode 100644 index 0000000000..17156d1068 --- /dev/null +++ b/gateway/examples/mistral-openai-proxy.yaml @@ -0,0 +1,43 @@ +# -------------------------------------------------------------------- +# OpenAI -> Mistral LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-mistral` policy adapts to Mistral's chat completions API +# and forwards to the existing `mistral-provider` upstream +# (api.mistral.ai). +# +# Single-provider mode: there is no router in front of the policy, so +# nothing sets metadata["selected_provider"] and the translator runs on +# every request. "id" is omitted, so it routes to the default upstream +# (the primary provider above). +# +# The provider must already be deployed and must have its Authorization +# bearer token set on upstream.auth (the policy does NOT inject the +# Mistral key). +# +# The provider example's api-key-auth policy expects the proxy to send the +# issued provider loopback key in X-API-Key. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-mistral +spec: + displayName: OpenAI to Mistral Proxy + version: v1.0 + context: /openai-mistral + provider: + id: mistral-provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + policies: + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest diff --git a/gateway/examples/mistral-provider.yaml b/gateway/examples/mistral-provider.yaml new file mode 100644 index 0000000000..67ec1c2654 --- /dev/null +++ b/gateway/examples/mistral-provider.yaml @@ -0,0 +1,63 @@ +# -------------------------------------------------------------------- +# Mistral LLM Provider Configuration +# +# Uses the built-in `mistralai` template and points to the public +# Mistral API endpoint. Replace the placeholder with a real Mistral key. +# +# The openai-to-mistral policy on the proxy layer translates +# OpenAI-format requests into Mistral's wire format (path +# /v1/chat/completions, model override, drop unsupported fields). +# +# Auth: upstream.auth is the gateway -> Mistral vendor auth. Proxies in +# front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: mistral-provider +spec: + displayName: Mistral Provider + version: v1.0 + template: mistralai + upstream: + url: https://api.mistral.ai + auth: + type: api-key + header: Authorization + value: $dxxxd + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/chat/completions + methods: [POST] + - path: /v1/embeddings + methods: [POST] + - path: /v1/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: mistral-provider + displayName: Mistral provider loopback key + apiKey: + value: mistral_provider_loopback_key_1234567890abcdef + maskedApiKey: mistral_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/openai-multi-provider-proxy.yaml b/gateway/examples/openai-multi-provider-proxy.yaml new file mode 100644 index 0000000000..a8ba025970 --- /dev/null +++ b/gateway/examples/openai-multi-provider-proxy.yaml @@ -0,0 +1,151 @@ +# -------------------------------------------------------------------- +# OpenAI Multi-Provider Proxy +# +# Exposes one OpenAI-shaped /chat/completions endpoint and fans out to +# several LLM providers. Two modes are supported by the translators: +# +# * Single-provider mode — attach exactly one translator with no +# router in front of it. With no "selected_provider" in the request +# metadata the translator runs unconditionally. +# +# * Multi-provider mode — put openai-header-router first. It writes +# the chosen provider into metadata["selected_provider"]. Each +# translator then runs only when that selection equals its own "id". +# The "id" doubles as the upstream cluster name, so it must match an +# entry in additionalProviders (id or as). +# +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-multi +spec: + displayName: OpenAI Multi-Provider Proxy + version: v1.0 + context: /openai-multi + provider: + id: openai-provider + auth: + type: api-key + header: X-API-Key + value: openai_provider_loopback_key_1234567890abcdef + additionalProviders: + - id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + - id: azure-openai-provider + auth: + type: api-key + header: X-API-Key + value: azure_openai_provider_loopback_key_1234567890abcdef + - id: mistral-provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + #mistral_provider_loopback_key_1234567890abcdef + # - id: gemini-provider + # auth: + # type: api-key + # header: X-API-Key + # value: gemini_provider_loopback_key_1234567890abcdef + + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - name: openai-header-router + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + headerName: x-provider + defaultProvider: openai-provider + mappings: + - headerValue: openai + provider: openai-provider + - headerValue: anthropic + provider: anthropic-provider + - headerValue: azure-openai + provider: azure-openai-provider + - headerValue: mistral + provider: mistral-provider + # - headerValue: gemini + # provider: gemini-provider + + # - name: token-based-ratelimit + # version: v1 + # paths: + # - path: /chat/completions + # methods: [POST] + # params: + # totalTokenLimits: + # - count: 40 + # duration: "1m" + # algorithm: fixed-window + # backend: memory + + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 + id: anthropic-provider + + - name: openai-to-azure-openai + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + apiVersion: "2024-02-15-preview" + model: gpt-4o + id: azure-openai-provider + + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + id: mistral-provider + + # - name: openai-to-gemini + # version: v1 + # paths: + # - path: /chat/completions + # methods: [POST] + # params: + # model: gemini-2.5-flash + # id: gemini-provider + # apiVersion: v1beta + +--- +# Client API key for this proxy. The api-key-auth policy above validates +# the X-API-Key header against the key the gateway issues for this resource. +# apiKey is omitted here, so the gateway generates one. Read it from the +# create response (POST .../llm-proxies/openai-multi/api-keys). +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-multi-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-multi + displayName: OpenAI multi-proxy consumer key + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/openai-provider.yaml b/gateway/examples/openai-provider.yaml new file mode 100644 index 0000000000..b206cff3f2 --- /dev/null +++ b/gateway/examples/openai-provider.yaml @@ -0,0 +1,67 @@ +# -------------------------------------------------------------------- +# OpenAI LLM Provider Configuration +# +# Uses the built-in `openai` template and points to the OpenAI public +# endpoint. The auth value below is a SAMPLE / placeholder token -- +# replace it with a real OpenAI API key (Bearer sk-...) before deploying. +# +# Auth: upstream.auth is the gateway -> OpenAI vendor auth. This is the +# only auth this provider needs; proxies in front of it route freely and +# inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: openai-provider +spec: + displayName: OpenAI Provider + version: v1.0 + template: openai + upstream: + url: https://api.openai.com/v1 + auth: + type: api-key + header: Authorization + # OpenAI expects: Authorization: Bearer -- sent verbatim, so the + # "Bearer " prefix MUST be included here. + value: Bearer sk-REPLACE_WITH_OPENAI_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /chat/completions + methods: [POST] + - path: /completions + methods: [POST] + - path: /embeddings + methods: [POST] + - path: /models + methods: [GET] + - path: /models/{modelId} + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: openai-provider + displayName: OpenAI provider loopback key + apiKey: + value: openai_provider_loopback_key_1234567890abcdef + maskedApiKey: openai_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/gateway-controller/api/management-openapi.yaml b/gateway/gateway-controller/api/management-openapi.yaml index bddc741df3..1d8903440a 100644 --- a/gateway/gateway-controller/api/management-openapi.yaml +++ b/gateway/gateway-controller/api/management-openapi.yaml @@ -5645,6 +5645,32 @@ components: auth: $ref: '#/components/schemas/LLMUpstreamAuth' + LLMProxyAdditionalProvider: + type: object + required: + - id + description: > + Additional LLM provider attached to this proxy as a selectable upstream. + Policies route to it by referring to the `as` name (defaults to `id`). + Optional auth config is used by the proxy when calling a protected + LlmProvider over the internal loopback route. + properties: + id: + type: string + description: Unique id of a deployed llm provider + example: anthropic-provider + as: + type: string + description: > + Logical upstream name used by policies to select this provider. Must + be unique within the proxy. Defaults to `id` when omitted. + pattern: '^[a-zA-Z0-9\-_]+$' + minLength: 1 + maxLength: 100 + example: anthropic-upstream + auth: + $ref: '#/components/schemas/LLMUpstreamAuth' + LLMAccessControl: type: object required: @@ -5828,6 +5854,15 @@ components: example: "api.openai.com" provider: $ref: '#/components/schemas/LLMProxyProvider' + additionalProviders: + type: array + description: > + Optional list of additional LLM providers attached to this proxy as + selectable upstreams. Policies (e.g. an OpenAI translator) can route + requests to any of these by setting the upstream name. The primary + `provider` field above remains the default upstream and the FK target. + items: + $ref: '#/components/schemas/LLMProxyAdditionalProvider' policies: type: array description: List of policies applied only to this operation (overrides or adds to API-level policies) diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index f5f5563199..e0ff53066d 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -911,8 +911,21 @@ type LLMProviderTemplateResourceMappings struct { Resources *[]LLMProviderTemplateResourceMapping `json:"resources,omitempty" yaml:"resources,omitempty"` } +// LLMProxyAdditionalProvider Additional LLM provider attached to this proxy as a selectable upstream. Policies route to it by referring to the `as` name (defaults to `id`). Optional auth config is used by the proxy when calling a protected LlmProvider over the internal loopback route. +type LLMProxyAdditionalProvider struct { + // As Logical upstream name used by policies to select this provider. Must be unique within the proxy. Defaults to `id` when omitted. + As *string `json:"as,omitempty" yaml:"as,omitempty"` + Auth *LLMUpstreamAuth `json:"auth,omitempty" yaml:"auth,omitempty"` + + // Id Unique id of a deployed llm provider + Id string `json:"id" yaml:"id"` +} + // LLMProxyConfigData defines model for LLMProxyConfigData. type LLMProxyConfigData struct { + // AdditionalProviders Optional list of additional LLM providers attached to this proxy as selectable upstreams. Policies (e.g. an OpenAI translator) can route requests to any of these by setting the upstream name. The primary `provider` field above remains the default upstream and the FK target. + AdditionalProviders *[]LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" yaml:"additionalProviders,omitempty"` + // Context Base path for all API routes (must start with /, no trailing slash) Context *string `json:"context,omitempty" yaml:"context,omitempty"` @@ -5003,261 +5016,266 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/gateway/gateway-controller/pkg/config/llm_validator.go b/gateway/gateway-controller/pkg/config/llm_validator.go index 62fed822ac..cb44132b63 100644 --- a/gateway/gateway-controller/pkg/config/llm_validator.go +++ b/gateway/gateway-controller/pkg/config/llm_validator.go @@ -565,7 +565,82 @@ func (v *LLMValidator) validateProxyData(spec *api.LLMProxyConfigData) []Validat Message: "spec.provider.id must consist of lowercase alphanumeric characters, hyphens, or dots, and must start and end with an alphanumeric character", }) } + if spec.Provider.Auth != nil { + errors = append(errors, v.validateLLMUpstreamAuth("spec.provider.auth", spec.Provider.Auth)...) + } + + if spec.AdditionalProviders != nil { + seen := map[string]bool{spec.Provider.Id: true} + for i, provider := range *spec.AdditionalProviders { + fieldPrefix := fmt.Sprintf("spec.additionalProviders[%d]", i) + if provider.Id == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".id", + Message: "Provider is required", + }) + } else if !v.metadataNameRegex.MatchString(provider.Id) { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".id", + Message: fieldPrefix + ".id must consist of lowercase alphanumeric characters, hyphens, or dots, and must start and end with an alphanumeric character", + }) + } + + upstreamName := provider.Id + if provider.As != nil && *provider.As != "" { + upstreamName = *provider.As + if !regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString(upstreamName) { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".as", + Message: fieldPrefix + ".as must contain only letters, numbers, hyphens, or underscores", + }) + } + } + if upstreamName != "" { + if seen[upstreamName] { + errors = append(errors, ValidationError{ + Field: fieldPrefix, + Message: fmt.Sprintf("duplicate upstream name '%s' in additionalProviders", upstreamName), + }) + } + seen[upstreamName] = true + } + + if provider.Auth != nil { + errors = append(errors, v.validateLLMUpstreamAuth(fieldPrefix+".auth", provider.Auth)...) + } + } + } + + return errors +} +func (v *LLMValidator) validateLLMUpstreamAuth(fieldPrefix string, auth *api.LLMUpstreamAuth) []ValidationError { + var errors []ValidationError + if auth.Type == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".type", + Message: "Auth type is required", + }) + } else if auth.Type != api.LLMUpstreamAuthTypeApiKey { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".type", + Message: "Auth type must be 'api-key'", + }) + } + if auth.Type == api.LLMUpstreamAuthTypeApiKey { + if auth.Header == nil || *auth.Header == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".header", + Message: "Auth header is required when api-key auth type is set", + }) + } + if auth.Value == nil || *auth.Value == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".value", + Message: "Auth value is required when api-key auth type is set", + }) + } + } return errors } diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer.go b/gateway/gateway-controller/pkg/utils/llm_transformer.go index 24d5da1b4e..4659341ab0 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer.go @@ -148,6 +148,50 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration spec.Upstream.Main = api.Upstream{ Url: &upstream, } + + // Step 3.1: Resolve additional providers (multi-provider proxies). Each is + // exposed as a named UpstreamDefinition so policies can route to it via + // the loopback context. The primary provider above remains the default. + if proxy.Spec.AdditionalProviders != nil && len(*proxy.Spec.AdditionalProviders) > 0 { + seen := map[string]bool{proxy.Spec.Provider.Id: true} + var defs []api.UpstreamDefinition + for _, ap := range *proxy.Spec.AdditionalProviders { + if ap.Id == "" { + return nil, fmt.Errorf("additionalProviders entry must have a non-empty id") + } + name := ap.Id + if ap.As != nil && *ap.As != "" { + name = *ap.As + } + if seen[name] { + return nil, fmt.Errorf("duplicate upstream name '%s' in additionalProviders (must be unique within the proxy and not collide with the primary provider id)", name) + } + seen[name] = true + + addCfg, err := t.db.GetConfigByKindAndHandle(string(api.LLMProviderConfigurationKindLlmProvider), ap.Id) + if err != nil { + return nil, fmt.Errorf("failed to look up additional provider '%s': %w", ap.Id, err) + } + if addCfg == nil { + return nil, fmt.Errorf("additional provider '%s' not found", ap.Id) + } + addCtx, err := addCfg.GetContext() + if err != nil { + return nil, fmt.Errorf("failed to get context for additional provider '%s': %w", ap.Id, err) + } + addURL := fmt.Sprintf("%s://%s:%d%s", + constants.SchemeHTTP, constants.LocalhostIP, t.routerConfig.ListenerPort, addCtx) + defs = append(defs, api.UpstreamDefinition{ + Name: name, + Upstreams: []struct { + Url string `json:"url" yaml:"url"` + Weight *int `json:"weight,omitempty" yaml:"weight,omitempty"` + }{{Url: addURL}}, + }) + } + spec.UpstreamDefinitions = &defs + } + // If provider has vhost configured add a host adding policy if providerConfig.Spec.Vhost != nil && *providerConfig.Spec.Vhost != "" { providerVhost := *providerConfig.Spec.Vhost @@ -182,34 +226,32 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration } // Step 3.5: Apply proxy-level provider auth for proxy->provider loopback upstream - var upstreamAuthPolicy *api.Policy + var upstreamAuthPolicies []api.Policy if proxy.Spec.Provider.Auth != nil { - auth := proxy.Spec.Provider.Auth - switch auth.Type { - case api.LLMUpstreamAuthTypeApiKey: - if auth.Value == nil || *auth.Value == "" { - return nil, fmt.Errorf("provider.auth.value is required") - } - header := "" - if auth.Header != nil { - header = *auth.Header + pol, err := t.proxyUpstreamAuthPolicy(proxy.Spec.Provider.Auth, "provider.auth") + if err != nil { + return nil, err + } + condition := selectedProviderExecutionCondition(proxy.Spec.Provider.Id, true) + pol.ExecutionCondition = &condition + upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) + } + if proxy.Spec.AdditionalProviders != nil { + for _, ap := range *proxy.Spec.AdditionalProviders { + if ap.Auth == nil { + continue } - params, err := GetUpstreamAuthApikeyPolicyParams(header, *auth.Value) - if err != nil { - return nil, fmt.Errorf("failed to build upstream auth params: %w", err) + name := ap.Id + if ap.As != nil && *ap.As != "" { + name = *ap.As } - policyVersion, err := t.resolvePolicyVersion(constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME) + pol, err := t.proxyUpstreamAuthPolicy(ap.Auth, fmt.Sprintf("additionalProviders[%s].auth", name)) if err != nil { return nil, err } - mh := api.Policy{ - Name: constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME, - Version: policyVersion, - Params: ¶ms, - } - upstreamAuthPolicy = &mh - default: - return nil, fmt.Errorf("unsupported upstream auth type: %s", auth.Type) + condition := selectedProviderExecutionCondition(name, false) + pol.ExecutionCondition = &condition + upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) } } @@ -279,14 +321,10 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration ops = append(ops, *op) } ops = sortOperationsBySpecificity(ops) - if upstreamAuthPolicy != nil { + if len(upstreamAuthPolicies) > 0 { for i := range ops { - if ops[i].Policies == nil { - ops[i].Policies = &[]api.Policy{*upstreamAuthPolicy} - } else { - existing := *ops[i].Policies - existing = append(existing, *upstreamAuthPolicy) - ops[i].Policies = &existing + for _, upstreamAuthPolicy := range upstreamAuthPolicies { + appendOperationPolicy(&ops[i], upstreamAuthPolicy) } } } @@ -616,6 +654,44 @@ func GetUpstreamAuthApikeyPolicyParams(header, value string) (map[string]interfa return m, nil } +func (t *LLMProviderTransformer) proxyUpstreamAuthPolicy(auth *api.LLMUpstreamAuth, field string) (*api.Policy, error) { + if auth == nil { + return nil, nil + } + switch auth.Type { + case api.LLMUpstreamAuthTypeApiKey: + if auth.Header == nil || *auth.Header == "" { + return nil, fmt.Errorf("%s.header is required", field) + } + if auth.Value == nil || *auth.Value == "" { + return nil, fmt.Errorf("%s.value is required", field) + } + params, err := GetUpstreamAuthApikeyPolicyParams(*auth.Header, *auth.Value) + if err != nil { + return nil, fmt.Errorf("failed to build upstream auth params: %w", err) + } + policyVersion, err := t.resolvePolicyVersion(constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME) + if err != nil { + return nil, err + } + return &api.Policy{ + Name: constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME, + Version: policyVersion, + Params: ¶ms, + }, nil + default: + return nil, fmt.Errorf("unsupported upstream auth type: %s", auth.Type) + } +} + +func selectedProviderExecutionCondition(providerName string, includeDefault bool) string { + selectedExpr := fmt.Sprintf("request.Metadata['selected_provider'] == '%s'", providerName) + if includeDefault { + return fmt.Sprintf("!('selected_provider' in request.Metadata) || %s", selectedExpr) + } + return fmt.Sprintf("'selected_provider' in request.Metadata && %s", selectedExpr) +} + // GetHostAdditionPolicyParams renders the policy params with given host value (host-rewrite) func GetHostAdditionPolicyParams(value string) (map[string]interface{}, error) { rendered := fmt.Sprintf(constants.PROXY_HOST__HEADER_POLICY_PARAMS, value) diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go index 3ff5426d44..dd31001493 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go @@ -200,6 +200,119 @@ func TestLLMProviderTransformer_TransformProxy_ReadsProviderAndTemplateFromDB(t assert.Equal(t, "http://127.0.0.1:8080/db-provider", *result.Spec.Upstream.Main.Url) } +func TestLLMProviderTransformer_TransformProxy_AdditionalProviderAuthIsConditional(t *testing.T) { + store := storage.NewConfigStore() + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + db := newTestSQLiteStorage(t, logger) + + template := &models.StoredLLMProviderTemplate{ + UUID: "0000-db-template-id-0000-000000000002", + Configuration: api.LLMProviderTemplate{ + ApiVersion: api.LLMProviderTemplateApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderTemplateKindLlmProviderTemplate, + Metadata: api.Metadata{Name: "openai"}, + Spec: api.LLMProviderTemplateData{DisplayName: "openai"}, + }, + } + require.NoError(t, db.SaveLLMProviderTemplate(template)) + + saveProvider := func(name, context string) { + providerSourceConfig := api.LLMProviderConfiguration{ + ApiVersion: api.LLMProviderConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderConfigurationKindLlmProvider, + Metadata: api.Metadata{Name: name}, + Spec: api.LLMProviderConfigData{ + DisplayName: name, + Version: "v1.0", + Context: stringPtr(context), + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://example.com")}, + AccessControl: api.LLMAccessControl{Mode: api.AllowAll}, + }, + } + require.NoError(t, db.SaveConfig(&models.StoredConfig{ + UUID: name + "-uuid", + Kind: string(api.LLMProviderConfigurationKindLlmProvider), + Handle: name, + DisplayName: name, + Version: "v1.0", + SourceConfiguration: providerSourceConfig, + DesiredState: models.StateDeployed, + })) + } + saveProvider("openai-provider", "/openai-provider") + saveProvider("anthropic-provider", "/anthropic-provider") + + transformer := NewLLMProviderTransformer(store, db, &config.RouterConfig{ListenerPort: 8080}, newTestPolicyVersionResolver()) + + proxy := &api.LLMProxyConfiguration{ + ApiVersion: api.LLMProxyConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProxyConfigurationKindLlmProxy, + Metadata: api.Metadata{Name: "openai-multi"}, + Spec: api.LLMProxyConfigData{ + DisplayName: "openai-multi", + Version: "v1.0", + Provider: api.LLMProxyProvider{ + Id: "openai-provider", + Auth: &api.LLMUpstreamAuth{ + Type: api.LLMUpstreamAuthTypeApiKey, + Header: stringPtr("Authorization"), + Value: stringPtr("Bearer primary"), + }, + }, + AdditionalProviders: &[]api.LLMProxyAdditionalProvider{{ + Id: "anthropic-provider", + Auth: &api.LLMUpstreamAuth{ + Type: api.LLMUpstreamAuthTypeApiKey, + Header: stringPtr("X-Provider-Key"), + Value: stringPtr("anthropic-loopback"), + }, + }}, + Policies: &[]api.LLMPolicy{{ + Name: "openai-header-router", + Version: "v1", + Paths: []api.LLMPolicyPath{{ + Path: "/chat/completions", + Methods: []api.LLMPolicyPathMethods{"POST"}, + Params: map[string]interface{}{ + "defaultProvider": "openai-provider", + }, + }}, + }}, + }, + } + + result, err := transformer.Transform(proxy, &api.RestAPI{}) + require.NoError(t, err) + require.NotNil(t, result.Spec.UpstreamDefinitions) + require.Len(t, *result.Spec.UpstreamDefinitions, 1) + assert.Equal(t, "anthropic-provider", (*result.Spec.UpstreamDefinitions)[0].Name) + + var chatOp *api.Operation + for i := range result.Spec.Operations { + if result.Spec.Operations[i].Path == "/chat/completions" && result.Spec.Operations[i].Method == "POST" { + chatOp = &result.Spec.Operations[i] + break + } + } + require.NotNil(t, chatOp) + require.NotNil(t, chatOp.Policies) + + var authPolicies []api.Policy + for _, pol := range *chatOp.Policies { + if pol.Name == constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME { + authPolicies = append(authPolicies, pol) + } + } + require.Len(t, authPolicies, 2) + require.NotNil(t, authPolicies[0].ExecutionCondition) + require.NotNil(t, authPolicies[1].ExecutionCondition) + assert.Contains(t, *authPolicies[0].ExecutionCondition, "openai-provider") + assert.Contains(t, *authPolicies[1].ExecutionCondition, "anthropic-provider") + assert.Equal(t, "Bearer primary", firstRequestHeaderValue(t, authPolicies[0].Params)) + assert.Equal(t, "anthropic-loopback", firstRequestHeaderValue(t, authPolicies[1].Params)) +} + func TestGetUpstreamAuthApikeyPolicyParams_Extended(t *testing.T) { t.Run("Valid parameters", func(t *testing.T) { params, err := GetUpstreamAuthApikeyPolicyParams("Authorization", "Bearer token123") @@ -215,6 +328,21 @@ func TestGetUpstreamAuthApikeyPolicyParams_Extended(t *testing.T) { }) } +func firstRequestHeaderValue(t *testing.T, params *map[string]interface{}) string { + t.Helper() + require.NotNil(t, params) + request, ok := (*params)["request"].(map[string]interface{}) + require.True(t, ok) + headers, ok := request["headers"].([]interface{}) + require.True(t, ok) + require.NotEmpty(t, headers) + header, ok := headers[0].(map[string]interface{}) + require.True(t, ok) + value, ok := header["value"].(string) + require.True(t, ok) + return value +} + func TestGetHostAdditionPolicyParams(t *testing.T) { t.Run("Valid host value", func(t *testing.T) { params, err := GetHostAdditionPolicyParams("api.example.com") diff --git a/gateway/sample-policies/slugify-body/policy-definition.yaml b/gateway/sample-policies/slugify-body/policy-definition.yaml index f19a90c78c..68882e2e99 100644 --- a/gateway/sample-policies/slugify-body/policy-definition.yaml +++ b/gateway/sample-policies/slugify-body/policy-definition.yaml @@ -27,22 +27,9 @@ name: slugify-body version: v1.0.0 displayName: Slugify Body description: | - Extracts a string value from the JSON request body at the configured JSONPath, - converts it into a URL-friendly slug using the `python-slugify` library, and - writes the slug back into the payload before forwarding to the upstream - service. - - Typical use cases include: - - Generating URL slugs from user-supplied titles or names. - - Normalising free-text identifiers into safe, lowercase, ASCII strings. - - Cleaning up input data at the gateway layer before it reaches backends. - - This is a **sample policy** that demonstrates: - 1. Using a third-party PyPI dependency (`python-slugify`). - 2. Reading and modifying JSON request bodies (buffered mode). - 3. Declaring configurable parameters with JSON Schema validation. - 4. Returning modified request bodies via `UpstreamRequestModifications`. - 5. Returning early error responses via `ImmediateResponse`. + Extracts a string from the JSON request body, converts it into a URL-friendly + slug, and writes the slug back before forwarding upstream. Demonstrates a + Python policy with third-party dependencies and JSON Schema parameters. # --- User-configurable parameters -------------------------------------------- # The `parameters` section is a full JSON Schema object. Every key under diff --git a/gateway/system-policies/analytics/policy-definition.yaml b/gateway/system-policies/analytics/policy-definition.yaml index 32b3f8ed25..5dece99292 100644 --- a/gateway/system-policies/analytics/policy-definition.yaml +++ b/gateway/system-policies/analytics/policy-definition.yaml @@ -1,10 +1,9 @@ name: wso2_apip_sys_analytics version: v1.0.0 description: | - Collects analytics data from request and response flows. This gets attached even - without any policies in the chain. Inspects headers and, optionally, bodies to - populate analytics metadata that is emitted via Envoy dynamic metadata and consumed - by the policy engine analytics pipeline and publishers. + Collects request and response analytics metadata from gateway traffic, + including optional payload capture, for the policy engine analytics pipeline + and publishers. parameters: type: object diff --git a/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go b/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go index 046f492af5..b4b29e2ee4 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go +++ b/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go @@ -32,6 +32,23 @@ type LLMProxyProvider struct { Auth *LLMUpstreamAuth `json:"auth,omitempty"` } +// LLMProxyAdditionalProvider references an additional LlmProvider that this +// proxy can route to by policy-selected upstream name. +type LLMProxyAdditionalProvider struct { + // Id is the LlmProvider handle (metadata.name). + // +kubebuilder:validation:Required + Id string `json:"id"` + + // As is the logical upstream name used by policies. Defaults to Id. + // +optional + As *string `json:"as,omitempty"` + + // Auth optionally configures credentials for proxy-to-provider loopback + // calls when the referenced provider is protected by an auth policy. + // +optional + Auth *LLMUpstreamAuth `json:"auth,omitempty"` +} + // LLMProxyConfigData mirrors the management-API LLMProxyConfigData payload. type LLMProxyConfigData struct { // DisplayName is a human-readable LLM proxy name. @@ -47,6 +64,11 @@ type LLMProxyConfigData struct { // +kubebuilder:validation:Required Provider LLMProxyProvider `json:"provider"` + // AdditionalProviders are extra LLM providers attached as selectable + // upstreams for multi-provider routing. + // +optional + AdditionalProviders []LLMProxyAdditionalProvider `json:"additionalProviders,omitempty"` + // Context is the base path for routes (must start with /). // +optional // +kubebuilder:validation:Pattern=`^/[a-zA-Z0-9\-._~!$&'()*+,;=:@%/]*[^/]$` diff --git a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go index b6163d5fd5..cfeda2ef43 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -965,6 +965,13 @@ func (in *LLMProviderUpstream) DeepCopy() *LLMProviderUpstream { func (in *LLMProxyConfigData) DeepCopyInto(out *LLMProxyConfigData) { *out = *in in.Provider.DeepCopyInto(&out.Provider) + if in.AdditionalProviders != nil { + in, out := &in.AdditionalProviders, &out.AdditionalProviders + *out = make([]LLMProxyAdditionalProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Context != nil { in, out := &in.Context, &out.Context *out = new(string) @@ -999,6 +1006,31 @@ func (in *LLMProxyConfigData) DeepCopy() *LLMProxyConfigData { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LLMProxyAdditionalProvider) DeepCopyInto(out *LLMProxyAdditionalProvider) { + *out = *in + if in.As != nil { + in, out := &in.As, &out.As + *out = new(string) + **out = **in + } + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(LLMUpstreamAuth) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LLMProxyAdditionalProvider. +func (in *LLMProxyAdditionalProvider) DeepCopy() *LLMProxyAdditionalProvider { + if in == nil { + return nil + } + out := new(LLMProxyAdditionalProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LLMProxyProvider) DeepCopyInto(out *LLMProxyProvider) { *out = *in diff --git a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml index dd5a2a5579..db0259fde6 100644 --- a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml +++ b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml @@ -55,6 +55,85 @@ spec: displayName: description: DisplayName is a human-readable LLM proxy name. type: string + additionalProviders: + description: |- + AdditionalProviders are extra LLM providers attached as selectable + upstreams for multi-provider routing. + items: + description: |- + LLMProxyAdditionalProvider references an additional LlmProvider that this + proxy can route to by policy-selected upstream name. + properties: + as: + description: As is the logical upstream name used by policies. + Defaults to Id. + type: string + auth: + description: |- + Auth optionally configures credentials for proxy-to-provider loopback + calls when the referenced provider is protected by an auth policy. + properties: + header: + description: |- + Header is the HTTP header to set on outbound requests when the auth + type uses headers (e.g. api-key). + type: string + type: + description: Type identifies the auth scheme. Only api-key + is currently supported. + enum: + - api-key + type: string + value: + description: |- + Value sources the credential. Exactly one of value or valueFrom must + be set. + properties: + value: + description: |- + Value is the inline plaintext value. Avoid for production use; prefer + valueFrom. + type: string + valueFrom: + description: ValueFrom references a Kubernetes Secret key. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: exactly one of value or valueFrom must be + set + rule: has(self.value) != has(self.valueFrom) + required: + - type + - value + type: object + id: + description: Id is the LlmProvider handle (metadata.name). + type: string + required: + - id + type: object + type: array policies: description: Policies is the list of policies applied to this LLM proxy. diff --git a/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go b/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go index 987b84a056..2d68ea5986 100644 --- a/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go +++ b/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go @@ -159,20 +159,38 @@ func (a *llmProxyAdapter) Deploy(ctx context.Context, k8sClient client.Client, g } specPayload := interface{}(spec) - if spec.Provider.Auth != nil { - resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.Provider.Auth, "spec.provider.auth.value") - if err != nil { - return DeployResult{}, err - } - if resolved == nil { - return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: provider.auth resolution produced nil")} - } + if spec.Provider.Auth != nil || len(spec.AdditionalProviders) > 0 { m, err := specToJSONMap(spec) if err != nil { return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} } - if err := flattenUpstreamAuthCredentialValue(m, "provider", *resolved); err != nil { - return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + if spec.Provider.Auth != nil { + resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.Provider.Auth, "spec.provider.auth.value") + if err != nil { + return DeployResult{}, err + } + if resolved == nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: provider.auth resolution produced nil")} + } + if err := flattenUpstreamAuthCredentialValue(m, "provider", *resolved); err != nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + } + } + for i := range spec.AdditionalProviders { + if spec.AdditionalProviders[i].Auth == nil { + continue + } + fieldPath := fmt.Sprintf("spec.additionalProviders[%d].auth.value", i) + resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.AdditionalProviders[i].Auth, fieldPath) + if err != nil { + return DeployResult{}, err + } + if resolved == nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: additionalProviders[%d].auth resolution produced nil", i)} + } + if err := flattenAdditionalProviderAuthCredentialValue(m, i, *resolved); err != nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + } } specPayload = m } diff --git a/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go b/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go index 6703f9c3dd..f6cec1c13c 100644 --- a/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go +++ b/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go @@ -93,3 +93,85 @@ func TestLlmProxyDeploy_ResolvesPolicyParamsValueFrom(t *testing.T) { require.True(t, strings.Contains(got, "name: api-key-auth")) } +func TestLlmProxyDeploy_ResolvesAdditionalProviderAuthValueFrom(t *testing.T) { + scheme := runtime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(apiv1.AddToScheme(scheme)) + + sec := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: "apigateway-demo", Name: "provider-secrets"}, + Data: map[string][]byte{ + "primary": []byte("Bearer primary-key"), + "additional": []byte("Bearer additional-key"), + }, + } + k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sec).Build() + + header := "Authorization" + authType := "api-key" + cr := &apiv1.LlmProxy{ + ObjectMeta: metav1.ObjectMeta{Name: "demo-llm-proxy", Namespace: "apigateway-demo"}, + Spec: apiv1.LLMProxyConfigData{ + DisplayName: "Lifecycle LLM Proxy", + Version: "v1.0", + Provider: apiv1.LLMProxyProvider{ + Id: "openai-provider", + Auth: &apiv1.LLMUpstreamAuth{ + Type: authType, + Header: &header, + Value: apiv1.SecretValueSource{ValueFrom: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "provider-secrets"}, + Key: "primary", + }}, + }, + }, + AdditionalProviders: []apiv1.LLMProxyAdditionalProvider{{ + Id: "anthropic-provider", + Auth: &apiv1.LLMUpstreamAuth{ + Type: authType, + Header: &header, + Value: apiv1.SecretValueSource{ValueFrom: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "provider-secrets"}, + Key: "additional", + }}, + }, + }}, + }, + } + + var ( + mu sync.Mutex + payload string + ) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + if r.Method == http.MethodPost { + b, _ := io.ReadAll(r.Body) + mu.Lock() + payload = string(b) + mu.Unlock() + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("{}")) + return + } + w.WriteHeader(http.StatusMethodNotAllowed) + })) + defer srv.Close() + + adapter := &llmProxyAdapter{} + _, err := adapter.Deploy(context.Background(), k8sClient, srv.URL, cr, nil) + require.NoError(t, err) + + mu.Lock() + got := payload + mu.Unlock() + require.Contains(t, got, "value: Bearer primary-key") + require.Contains(t, got, "value: Bearer additional-key") + require.Contains(t, got, "additionalProviders:") + require.NotContains(t, got, "secretKeyRef") + require.NotContains(t, got, "valueFrom") +} diff --git a/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go b/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go index e32c01e772..ffe5f609f9 100644 --- a/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go +++ b/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go @@ -42,3 +42,23 @@ func flattenUpstreamAuthCredentialValue(specMap map[string]interface{}, parentKe auth["value"] = plaintext return nil } + +func flattenAdditionalProviderAuthCredentialValue(specMap map[string]interface{}, index int, plaintext string) error { + additionalProviders, ok := specMap["additionalProviders"].([]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders must be an array") + } + if index < 0 || index >= len(additionalProviders) { + return fmt.Errorf("spec.additionalProviders[%d] is out of range", index) + } + parent, ok := additionalProviders[index].(map[string]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders[%d] must be an object", index) + } + auth, ok := parent["auth"].(map[string]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders[%d].auth must be an object", index) + } + auth["value"] = plaintext + return nil +} diff --git a/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go b/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go index 92e26fe455..7760063f47 100644 --- a/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go +++ b/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go @@ -218,6 +218,14 @@ func llmProxyReferencesSecret(cr *apiv1.LlmProxy, secretNS, secretName string) b return true } } + for i := range cr.Spec.AdditionalProviders { + auth := cr.Spec.AdditionalProviders[i].Auth + if auth != nil && auth.Value.ValueFrom != nil { + if cr.Namespace == secretNS && strings.TrimSpace(auth.Value.ValueFrom.Name) == secretName { + return true + } + } + } return llmProxyReferencesValueFromKind(cr, secretKeyRefKey, secretNS, secretName) } @@ -240,4 +248,3 @@ func llmProxyReferencesValueFromKind(cr *apiv1.LlmProxy, kind, targetNS, targetN } return false } - diff --git a/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go b/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go index 2c6a19068a..16bd4a42dd 100644 --- a/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go +++ b/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go @@ -110,6 +110,11 @@ func llmProxyExternalDepsFingerprint(ctx context.Context, c client.Client, cr *a if cr.Spec.Provider.Auth != nil { accumulateBackingFromSecretValueSource(cr.Spec.Provider.Auth.Value, cr.Namespace, backing) } + for i := range cr.Spec.AdditionalProviders { + if cr.Spec.AdditionalProviders[i].Auth != nil { + accumulateBackingFromSecretValueSource(cr.Spec.AdditionalProviders[i].Auth.Value, cr.Namespace, backing) + } + } for i := range cr.Spec.Policies { for j := range cr.Spec.Policies[i].Paths { if p := cr.Spec.Policies[i].Paths[j].Params; p != nil { @@ -139,4 +144,3 @@ func resolveRawExtensionValueFrom(ctx context.Context, c client.Client, defaultN raw.Raw = out return nil } - diff --git a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml index dd5a2a5579..db0259fde6 100644 --- a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml +++ b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml @@ -55,6 +55,85 @@ spec: displayName: description: DisplayName is a human-readable LLM proxy name. type: string + additionalProviders: + description: |- + AdditionalProviders are extra LLM providers attached as selectable + upstreams for multi-provider routing. + items: + description: |- + LLMProxyAdditionalProvider references an additional LlmProvider that this + proxy can route to by policy-selected upstream name. + properties: + as: + description: As is the logical upstream name used by policies. + Defaults to Id. + type: string + auth: + description: |- + Auth optionally configures credentials for proxy-to-provider loopback + calls when the referenced provider is protected by an auth policy. + properties: + header: + description: |- + Header is the HTTP header to set on outbound requests when the auth + type uses headers (e.g. api-key). + type: string + type: + description: Type identifies the auth scheme. Only api-key + is currently supported. + enum: + - api-key + type: string + value: + description: |- + Value sources the credential. Exactly one of value or valueFrom must + be set. + properties: + value: + description: |- + Value is the inline plaintext value. Avoid for production use; prefer + valueFrom. + type: string + valueFrom: + description: ValueFrom references a Kubernetes Secret key. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: exactly one of value or valueFrom must be + set + rule: has(self.value) != has(self.valueFrom) + required: + - type + - value + type: object + id: + description: Id is the LlmProvider handle (metadata.name). + type: string + required: + - id + type: object + type: array policies: description: Policies is the list of policies applied to this LLM proxy. diff --git a/mistral-proxy.yaml b/mistral-proxy.yaml new file mode 100644 index 0000000000..536a17c5b8 --- /dev/null +++ b/mistral-proxy.yaml @@ -0,0 +1,246 @@ +# ==================================================================== +# Mistral — Provider + Single Proxy + Consumer Key (docs 1-4) +# Plus a MULTI-PROVIDER proxy sample at the bottom (docs 5-6) +# +# THREE auth hops exist — keep them straight: +# (A) provider.upstream.auth -> provider -> Mistral (the Mistral key) +# (B) proxy api-key-auth -> client -> proxy (consumer key) +# (C) proxy.provider.auth -> proxy -> provider (loopback, optional) +# +# For every auth block: type MUST be "api-key" (never "x-api-key"); +# "x-api-key" is only ever a HEADER NAME, never a type. value non-empty. +# ==================================================================== + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: mistral-provider +spec: + displayName: Mistral Provider + version: v1.0 + template: mistralai + upstream: + url: https://api.mistral.ai + auth: + type: api-key # (A) only valid type for an LlmProvider + header: Authorization # Mistral expects: Authorization: Bearer + value: Bearer # <-- put your real Mistral key here + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/chat/completions + methods: [POST] + - path: /v1/embeddings + methods: [POST] + - path: /v1/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: mistral-provider + displayName: Mistral provider loopback key + apiKey: + value: mistral_provider_loopback_key_1234567890abcdef + maskedApiKey: mistral_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days + +--- +# Second provider used only by the MULTI-PROVIDER sample below. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: anthropic-provider +spec: + displayName: Anthropic Provider + version: v1.0 + template: anthropic + upstream: + url: https://api.anthropic.com + auth: + type: api-key # type is still "api-key"... + header: x-api-key # ...but Anthropic's HEADER is x-api-key (no Bearer) + value: + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/messages + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/messages + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: anthropic-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: anthropic-provider + displayName: Anthropic provider loopback key + apiKey: + value: anthropic_provider_loopback_key_1234567890abcdef + maskedApiKey: anthropic_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days + +--- +# ----- SINGLE-provider proxy ----------------------------------------- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-mistral +spec: + displayName: OpenAI to Mistral Proxy + version: v1.0 + context: /openai-mistral + provider: + id: mistral-provider + # (C) OPTIONAL proxy->provider loopback auth. A SEPARATE hop, NOT the + # Mistral key. Only needed if the provider API itself is secured. + # Corrected form: type "api-key" (NOT "x-api-key"), value non-empty. + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + policies: + - name: api-key-auth # (B) client -> proxy consumer key + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + +--- +# Consumer key for the single proxy. spec.apiKey omitted -> gateway generates it. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-to-mistral + displayName: Mistral proxy consumer key + expiresIn: + duration: 30 + unit: days + +--- +# ----- MULTI-provider proxy sample ----------------------------------- +# One OpenAI-shaped endpoint that fans out to Mistral OR Anthropic based +# on the `x-provider` request header (read by the openai-header-router +# policy). Each provider has its own LlmProvider ApiKey above; the proxy +# sends those loopback keys via provider.auth/additionalProviders[].auth. +# +# Routing: header x-provider: mistral -> mistral-provider +# header x-provider: anthropic -> anthropic-provider +# (absent/unmatched) -> defaultProvider (mistral) +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-multi +spec: + displayName: OpenAI Multi-Provider Proxy + version: v1.0 + context: /openai-multi + provider: + id: mistral-provider # primary/default provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + additionalProviders: + - id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + # 1) Selector: reads x-provider header, picks the provider. + - name: openai-header-router + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + headerName: x-provider + defaultProvider: mistral-provider + mappings: + - headerValue: mistral + provider: mistral-provider + - headerValue: anthropic + provider: anthropic-provider + # 2) Per-provider translators. Each runs only when its id is selected. + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + id: mistral-provider + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 + id: anthropic-provider + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-multi-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-multi + displayName: OpenAI multi-proxy consumer key + expiresIn: + duration: 30 + unit: days diff --git a/platform-api/src/api/generated.go b/platform-api/src/api/generated.go index 1aad9b6b27..ea5fe67ca9 100644 --- a/platform-api/src/api/generated.go +++ b/platform-api/src/api/generated.go @@ -1812,6 +1812,9 @@ type LLMProviderTemplateResourceMappings struct { // LLMProxy defines model for LLMProxy. type LLMProxy struct { + // AdditionalProviders Optional list of additional LLM providers attached to this proxy as selectable upstreams. Policies route requests to any of these by setting the upstream name. The primary `provider` field above remains the default upstream and the FK target. + AdditionalProviders *[]LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" yaml:"additionalProviders,omitempty"` + // Context Base path for all REST API routes (must start with /, no trailing slash) Context *string `json:"context,omitempty" yaml:"context,omitempty"` @@ -1862,6 +1865,15 @@ type LLMProxyAPIKeyListResponse struct { Items []APIKeyItem `binding:"required" json:"items" yaml:"items"` } +// LLMProxyAdditionalProvider Additional LLM provider attached to this proxy as a selectable upstream. Policies route to it by referring to the `as` name (defaults to `id`). +type LLMProxyAdditionalProvider struct { + // As Logical upstream name used by policies to select this provider. Must be unique within the proxy. Defaults to `id` when omitted. + As *string `json:"as,omitempty" yaml:"as,omitempty"` + + // Id Unique id of a deployed llm provider + Id string `binding:"required" json:"id" yaml:"id"` +} + // LLMProxyListItem defines model for LLMProxyListItem. type LLMProxyListItem struct { // Context Context path where the proxy is exposed diff --git a/platform-api/src/internal/dto/llm_deployment.go b/platform-api/src/internal/dto/llm_deployment.go index 9fc0c7a8a6..f8891ee962 100644 --- a/platform-api/src/internal/dto/llm_deployment.go +++ b/platform-api/src/internal/dto/llm_deployment.go @@ -59,15 +59,21 @@ type LLMProxyDeploymentYAML struct { // LLMProxyDeploymentSpec represents the spec section for LLM proxy deployments type LLMProxyDeploymentSpec struct { - DisplayName string `yaml:"displayName"` - Version string `yaml:"version"` - Context string `yaml:"context,omitempty"` - VHost string `yaml:"vhost,omitempty"` - Provider LLMProxyDeploymentProvider `yaml:"provider"` - Policies []api.LLMPolicy `yaml:"policies,omitempty"` + DisplayName string `yaml:"displayName"` + Version string `yaml:"version"` + Context string `yaml:"context,omitempty"` + VHost string `yaml:"vhost,omitempty"` + Provider LLMProxyDeploymentProvider `yaml:"provider"` + AdditionalProviders []LLMProxyDeploymentAdditionalProvider `yaml:"additionalProviders,omitempty"` + Policies []api.LLMPolicy `yaml:"policies,omitempty"` } type LLMProxyDeploymentProvider struct { ID string `yaml:"id"` Auth *api.UpstreamAuth `yaml:"auth,omitempty"` } + +type LLMProxyDeploymentAdditionalProvider struct { + ID string `yaml:"id"` + As string `yaml:"as,omitempty"` +} diff --git a/platform-api/src/internal/model/llm.go b/platform-api/src/internal/model/llm.go index 2332da5638..0b24f12e64 100644 --- a/platform-api/src/internal/model/llm.go +++ b/platform-api/src/internal/model/llm.go @@ -207,14 +207,24 @@ type LLMProxy struct { } type LLMProxyConfig struct { - Name string `json:"name,omitempty" db:"-"` - Version string `json:"version,omitempty" db:"-"` - Context *string `json:"context,omitempty" db:"-"` - Vhost *string `json:"vhost,omitempty" db:"-"` - Provider string `json:"provider,omitempty" db:"-"` - UpstreamAuth *UpstreamAuth `json:"upstreamAuth,omitempty" db:"-"` - Policies []LLMPolicy `json:"policies,omitempty" db:"-"` - Security *SecurityConfig `json:"security,omitempty" db:"-"` + Name string `json:"name,omitempty" db:"-"` + Version string `json:"version,omitempty" db:"-"` + Context *string `json:"context,omitempty" db:"-"` + Vhost *string `json:"vhost,omitempty" db:"-"` + Provider string `json:"provider,omitempty" db:"-"` + UpstreamAuth *UpstreamAuth `json:"upstreamAuth,omitempty" db:"-"` + AdditionalProviders []LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" db:"-"` + Policies []LLMPolicy `json:"policies,omitempty" db:"-"` + Security *SecurityConfig `json:"security,omitempty" db:"-"` +} + +// LLMProxyAdditionalProvider is an additional LLM provider attached to a proxy +// as a selectable upstream. Policies route to it by the `As` name (which +// defaults to `ID` when empty). Provider upstream auth is taken from the +// referenced LlmProvider's own configuration. +type LLMProxyAdditionalProvider struct { + ID string `json:"id" db:"-"` + As string `json:"as,omitempty" db:"-"` } type SecurityConfig struct { diff --git a/platform-api/src/internal/service/llm.go b/platform-api/src/internal/service/llm.go index 1c112626e8..2ed710213b 100644 --- a/platform-api/src/internal/service/llm.go +++ b/platform-api/src/internal/service/llm.go @@ -630,12 +630,13 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( OpenAPISpec: utils.ValueOrEmpty(req.Openapi), Status: llmStatusPending, Configuration: model.LLMProxyConfig{ - Context: &contextValue, - Vhost: req.Vhost, - Provider: req.Provider.Id, - UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), - Policies: mapPoliciesAPIToModel(req.Policies), - Security: mapSecurityAPIToModel(req.Security), + Context: &contextValue, + Vhost: req.Vhost, + Provider: req.Provider.Id, + UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), + AdditionalProviders: mapAdditionalProvidersAPIToModel(req.AdditionalProviders), + Policies: mapPoliciesAPIToModel(req.Policies), + Security: mapSecurityAPIToModel(req.Security), }, } @@ -842,12 +843,13 @@ func (s *LLMProxyService) Update(orgUUID, handle string, req *api.LLMProxy) (*ap OpenAPISpec: utils.ValueOrEmpty(req.Openapi), Status: llmStatusPending, Configuration: model.LLMProxyConfig{ - Context: &contextValue, - Vhost: req.Vhost, - Provider: req.Provider.Id, - UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), - Policies: mapPoliciesAPIToModel(req.Policies), - Security: mapSecurityAPIToModel(req.Security), + Context: &contextValue, + Vhost: req.Vhost, + Provider: req.Provider.Id, + UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), + AdditionalProviders: mapAdditionalProvidersAPIToModel(req.AdditionalProviders), + Policies: mapPoliciesAPIToModel(req.Policies), + Security: mapSecurityAPIToModel(req.Security), }, } @@ -1048,6 +1050,36 @@ func mapUpstreamAuthAPIToModel(in *api.UpstreamAuth) *model.UpstreamAuth { } } +func mapAdditionalProvidersAPIToModel(in *[]api.LLMProxyAdditionalProvider) []model.LLMProxyAdditionalProvider { + if in == nil || len(*in) == 0 { + return nil + } + out := make([]model.LLMProxyAdditionalProvider, 0, len(*in)) + for _, p := range *in { + out = append(out, model.LLMProxyAdditionalProvider{ + ID: p.Id, + As: utils.ValueOrEmpty(p.As), + }) + } + return out +} + +func mapAdditionalProvidersModelToAPI(in []model.LLMProxyAdditionalProvider) *[]api.LLMProxyAdditionalProvider { + if len(in) == 0 { + return nil + } + out := make([]api.LLMProxyAdditionalProvider, 0, len(in)) + for _, p := range in { + entry := api.LLMProxyAdditionalProvider{Id: p.ID} + if p.As != "" { + as := p.As + entry.As = &as + } + out = append(out, entry) + } + return &out +} + func normalizeUpstreamAuthType(authType string) string { normalized := strings.TrimSpace(authType) if normalized == "" { @@ -1857,6 +1889,9 @@ func mapProxyModelToAPI(m *model.LLMProxy) *api.LLMProxy { Value: nil, // Redact auth credential value } } + if extra := mapAdditionalProvidersModelToAPI(m.Configuration.AdditionalProviders); extra != nil { + out.AdditionalProviders = extra + } if len(m.Configuration.Policies) > 0 { policyList := make([]api.LLMPolicy, 0, len(m.Configuration.Policies)) for _, p := range m.Configuration.Policies { diff --git a/platform-api/src/internal/service/llm_deployment.go b/platform-api/src/internal/service/llm_deployment.go index 29cfa6523f..9c5edba7e2 100644 --- a/platform-api/src/internal/service/llm_deployment.go +++ b/platform-api/src/internal/service/llm_deployment.go @@ -1681,7 +1681,8 @@ func generateLLMProxyDeploymentYAML(proxy *model.LLMProxy) (string, error) { Provider: dto.LLMProxyDeploymentProvider{ ID: proxy.Configuration.Provider, }, - Policies: policies, + AdditionalProviders: mapAdditionalProvidersModelToDeployment(proxy.Configuration.AdditionalProviders), + Policies: policies, }, } @@ -1719,6 +1720,20 @@ func mapModelUpstreamAuthToAPI(auth *model.UpstreamAuth) *api.UpstreamAuth { return mapModelAuthToAPI(auth) } +func mapAdditionalProvidersModelToDeployment(in []model.LLMProxyAdditionalProvider) []dto.LLMProxyDeploymentAdditionalProvider { + if len(in) == 0 { + return nil + } + out := make([]dto.LLMProxyDeploymentAdditionalProvider, 0, len(in)) + for _, p := range in { + out = append(out, dto.LLMProxyDeploymentAdditionalProvider{ + ID: p.ID, + As: p.As, + }) + } + return out +} + // orderLLMPolicies ensures llm-cost-based-ratelimit always precedes llm-cost in the policy list. // All other policies retain their relative positions. func orderLLMPolicies(policies []api.LLMPolicy) []api.LLMPolicy { diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index 7e67243074..e5ce2a926d 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -9091,6 +9091,15 @@ components: example: "api.openai" provider: $ref: '#/components/schemas/LLMProxyProvider' + additionalProviders: + type: array + description: > + Optional list of additional LLM providers attached to this proxy as + selectable upstreams. Policies route requests to any of these by + setting the upstream name. The primary `provider` field above + remains the default upstream and the FK target. + items: + $ref: '#/components/schemas/LLMProxyAdditionalProvider' openapi: type: string description: OpenAPI specification (JSON or YAML) for the proxy endpoint @@ -9195,6 +9204,28 @@ components: auth: $ref: '#/components/schemas/UpstreamAuth' + LLMProxyAdditionalProvider: + type: object + required: + - id + description: > + Additional LLM provider attached to this proxy as a selectable upstream. + Policies route to it by referring to the `as` name (defaults to `id`). + properties: + id: + type: string + description: Unique id of a deployed llm provider + example: anthropic-provider + as: + type: string + description: > + Logical upstream name used by policies to select this provider. Must + be unique within the proxy. Defaults to `id` when omitted. + pattern: '^[a-zA-Z0-9\-_]+$' + minLength: 1 + maxLength: 100 + example: anthropic-upstream + CreateLLMProviderAPIKeyRequest: type: object properties: From d7860b6d8579d3c9af23559dff8b05a6bd1c7864 Mon Sep 17 00:00:00 2001 From: pamaljayasinghe Date: Wed, 27 May 2026 09:34:11 +0530 Subject: [PATCH 02/12] New changes --- .gitignore | 2 +- .../MULTI_PROVIDER_LLM_ROUTING.md | 285 + .../dev-policies/openai-header-router/go.mod | 5 + .../dev-policies/openai-header-router/go.sum | 2 + .../openaiheaderrouter.go | 289 + .../policy-definition.yaml | 60 + .../dev-policies/openai-to-anthropic/go.mod | 5 + .../dev-policies/openai-to-anthropic/go.sum | 2 + .../openai-to-anthropic/openaitoanthropic.go | 534 + .../policy-definition.yaml | 44 + .../openai-to-anthropic/response.go | 269 + .../openai-to-azure-openai/HOWTO.md | 212 + .../openai-to-azure-openai/go.mod | 5 + .../openai-to-azure-openai/go.sum | 2 + .../openaitoazureopenai.go | 211 + .../policy-definition.yaml | 53 + gateway/dev-policies/openai-to-gemini/go.mod | 5 + gateway/dev-policies/openai-to-gemini/go.sum | 2 + .../openai-to-gemini/openaitogemini.go | 606 + .../openai-to-gemini/policy-definition.yaml | 46 + .../dev-policies/openai-to-gemini/response.go | 330 + gateway/dev-policies/openai-to-mistral/go.mod | 5 + gateway/dev-policies/openai-to-mistral/go.sum | 2 + .../openai-to-mistral/openaitomistral.go | 235 + .../openai-to-mistral/policy-definition.yaml | 37 + .../openai-to-mistral/response.go | 153 + gateway/dev-policies/openaidefinition.yaml | 22725 ++++++++++++++++ 27 files changed, 26125 insertions(+), 1 deletion(-) create mode 100644 gateway/dev-policies/MULTI_PROVIDER_LLM_ROUTING.md create mode 100644 gateway/dev-policies/openai-header-router/go.mod create mode 100644 gateway/dev-policies/openai-header-router/go.sum create mode 100644 gateway/dev-policies/openai-header-router/openaiheaderrouter.go create mode 100644 gateway/dev-policies/openai-header-router/policy-definition.yaml create mode 100644 gateway/dev-policies/openai-to-anthropic/go.mod create mode 100644 gateway/dev-policies/openai-to-anthropic/go.sum create mode 100644 gateway/dev-policies/openai-to-anthropic/openaitoanthropic.go create mode 100644 gateway/dev-policies/openai-to-anthropic/policy-definition.yaml create mode 100644 gateway/dev-policies/openai-to-anthropic/response.go create mode 100644 gateway/dev-policies/openai-to-azure-openai/HOWTO.md create mode 100644 gateway/dev-policies/openai-to-azure-openai/go.mod create mode 100644 gateway/dev-policies/openai-to-azure-openai/go.sum create mode 100644 gateway/dev-policies/openai-to-azure-openai/openaitoazureopenai.go create mode 100644 gateway/dev-policies/openai-to-azure-openai/policy-definition.yaml create mode 100644 gateway/dev-policies/openai-to-gemini/go.mod create mode 100644 gateway/dev-policies/openai-to-gemini/go.sum create mode 100644 gateway/dev-policies/openai-to-gemini/openaitogemini.go create mode 100644 gateway/dev-policies/openai-to-gemini/policy-definition.yaml create mode 100644 gateway/dev-policies/openai-to-gemini/response.go create mode 100644 gateway/dev-policies/openai-to-mistral/go.mod create mode 100644 gateway/dev-policies/openai-to-mistral/go.sum create mode 100644 gateway/dev-policies/openai-to-mistral/openaitomistral.go create mode 100644 gateway/dev-policies/openai-to-mistral/policy-definition.yaml create mode 100644 gateway/dev-policies/openai-to-mistral/response.go create mode 100644 gateway/dev-policies/openaidefinition.yaml diff --git a/.gitignore b/.gitignore index 595464cb0c..ac6fd2e575 100644 --- a/.gitignore +++ b/.gitignore @@ -144,7 +144,7 @@ gateway/gateway-runtime/target gateway/gateway-controller/target/ gateway/gateway-builder/target/ event-gateway/gateway-runtime/target/ -dev-policies/ +# dev-policies/ # Copilot Prompts .github/prompts diff --git a/gateway/dev-policies/MULTI_PROVIDER_LLM_ROUTING.md b/gateway/dev-policies/MULTI_PROVIDER_LLM_ROUTING.md new file mode 100644 index 0000000000..34408a6ac2 --- /dev/null +++ b/gateway/dev-policies/MULTI_PROVIDER_LLM_ROUTING.md @@ -0,0 +1,285 @@ +# Multi-Provider LLM Routing & Provider Transformation + +> One OpenAI-shaped endpoint, many LLM backends. Clients keep talking plain +> OpenAI Chat Completions; the gateway picks a provider, rewrites the request +> into that provider's native API, calls it with its own credentials, and +> rewrites the response back into the OpenAI shape. + +--- + +## 1. What is this? + +This is the **LLM proxy layer** of the WSO2 API Platform gateway. It lets you +publish a single endpoint that speaks the **OpenAI Chat Completions** wire +format (`POST /chat/completions`) while the actual upstream can be OpenAI, +Azure OpenAI, Anthropic, Mistral, or Gemini. + +Two cooperating pieces make this work: + +1. **Routing policy** (`openai-header-router`) — decides *which* provider a + request should go to and publishes that decision. +2. **Transformation policies** (`openai-to-anthropic`, `openai-to-azure-openai`, + `openai-to-mistral`, `openai-to-gemini`) — translate the OpenAI request into + the selected provider's native format and translate the response back. + +The control plane (management API + Kubernetes operator) wires these together +from a single `LlmProxy` resource that lists a primary `provider` and any +number of `additionalProviders`. + +--- + +## 2. Why do we need it? + +Every LLM vendor exposes a *different* HTTP API: + +| Provider | Path shape | Auth header | Body shape | +|--------------|----------------------------------------------|--------------------|-----------------------| +| OpenAI | `/v1/chat/completions` | `Authorization: Bearer` | OpenAI messages | +| Azure OpenAI | `/openai/deployments/{model}/chat/completions?api-version=…` | `api-key` | OpenAI messages | +| Anthropic | `/v1/messages` | `x-api-key` | Anthropic messages | +| Mistral | `/v1/chat/completions` | `Authorization: Bearer` | OpenAI-ish | +| Gemini | `/{ver}/models/{model}:generateContent` | `x-goog-api-key` | `contents[]` + `parts`| + +Without this layer, **every client** would have to know each provider's quirks, +hold each provider's keys, and be rewritten whenever you switch or add a vendor. + +With this layer: + +- **Clients write OpenAI once.** Switching from OpenAI to Gemini is a config + change on the proxy, not a client change. +- **Provider credentials stay in the gateway.** Clients authenticate to the + gateway with one key; the gateway holds and injects each provider's real key + on the proxy→provider hop. +- **Provider selection is dynamic.** A single header (`x-provider`) can fan one + endpoint out to several vendors — useful for A/B testing, cost routing, + failover staging, and per-tenant provider choice. + +--- + +## 3. Architecture + +### 3.1 Big picture (control plane → gateway) + +``` + API PLATFORM (control plane) + ┌───────────────────────────────────────────────────────────────────┐ + │ API Designer / Management Portal / Dev Portal │ + │ │ │ + │ Platform API ──────────────► Gateway Controller │ + │ │ │ + │ Policy Engine ──► Router │ + │ ▲ │ + │ Provider Transformation Policy │ + └───────────────────────────────────────────────────────────────────┘ + ▲ pulls policy + Gateway Builder ◄── Pulls Policy ── Policy HUB +``` + +The operator/management side takes an `LlmProxy` spec and emits the concrete +route + policy chain the gateway runs. + +### 3.2 Request data flow (the bottom half of the diagram) + +Example: a client sends an **OpenAI** request, the proxy targets **Azure OpenAI**. + +``` + Client Gateway Policy Engine Upstream + (OpenAI ┌─────────────────────────────────────────────┐ + format) │ 1. api-key-auth validates client key │ + │ │ 2. openai-header-router picks provider, │ + ▼ │ writes metadata[selected_provider] │ + ┌──────┐ │ 3. upstream-auth injects provider key │ ┌─────────────┐ + │OpenAI│──►│ 4. openai-to-azure-openai │──►│ AzureOpenAI │──► azure.com + │ LLM │ │ • rewrites BODY → Azure shape │ │ (native API)│ + │Proxy │◄──│ • rewrites PATH → /openai/deployments/… │◄──│ │◄── response + └──────┘ │ • on response: Azure → OpenAI shape │ └─────────────┘ + ▲ └─────────────────────────────────────────────┘ + └── OpenAI-shaped response returned to client +``` + +The transformation is **bidirectional**: request OpenAI→provider on the way in, +response provider→OpenAI on the way out. + +--- + +## 4. The policies (`gateway/dev-policies/`) + +These are Go policies built against the gateway SDK +(`github.com/wso2/api-platform/sdk`). See [README.md](./README.md) for how +dev-policies are built and registered. + +### 4.1 `openai-header-router` — provider selection + +- **Phase:** runs in the **request-header** phase (and re-publishes in the body + phase as an idempotent fallback). +- **What it does:** reads a configured header (default `x-provider`), matches it + case-insensitively against an ordered `mappings` list, and writes the chosen + provider id into `SharedContext.Metadata["selected_provider"]`. Falls back to + `defaultProvider` when the header is missing or unmatched. +- **Why header phase:** the proxy→provider auth-injection policy is gated on + `selected_provider` and runs in the header phase — so the selection must + exist *before* it evaluates. (See `Mode()` doc comment in + [openaiheaderrouter.go](./openai-header-router/openaiheaderrouter.go).) + +Key params: + +| Param | Purpose | +|-------------------|-------------------------------------------------------------| +| `headerName` | Header to read (default `x-provider`). | +| `defaultProvider` | Provider id when header missing/unmatched. **Required.** | +| `mappings[]` | `headerValue → provider` rules, first match wins. **Required.** Duplicate header values are rejected. | + +### 4.2 `openai-to-{anthropic,azure-openai,mistral,gemini}` — translators + +Each translator: + +- **Phase:** buffers request body + response body. +- **Gating (`shouldRun`):** the heart of single- vs multi-provider mode — + - **Single-provider mode:** no router ran, so `selected_provider` is empty → + the translator **always runs**. + - **Multi-provider mode:** a router published a selection → the translator + runs **only if** `selected_provider == its own id` (case-insensitive). +- **Request translation:** rewrites the JSON body into the provider's native + shape and rewrites the upstream **path**; sets `UpstreamName` to its `id` so + the request routes to the right upstream cluster. +- **Response translation:** rewrites the provider's non-streaming response back + into an OpenAI `ChatCompletion`. Streaming (SSE) responses are passed through + untouched. + +Common params: + +| Param | Purpose | +|--------------|------------------------------------------------------------------| +| `model` | Provider model name used in the rewritten request. **Required.** | +| `id` | Provider this translator targets. Doubles as the upstream cluster name and the key matched against `selected_provider`. | +| `apiVersion` | (azure/gemini) API version segment in the rewritten path. | + +The Gemini translator ([openaitogemini.go](./openai-to-gemini/openaitogemini.go)) +is the most involved example: it maps OpenAI `messages[]` to Gemini +`contents[]`/`systemInstruction`, `tools`→`functionDeclarations`, +`tool_choice`→`functionCallingConfig`, sampling params→`generationConfig`, and +images (`image_url`) to `inlineData`/`fileData`. + +--- + +## 5. Control-plane wiring (`LlmProxy` resource) + +A proxy declares one primary `provider` and zero-or-more `additionalProviders`. +Each can carry its own loopback `auth` (the credential the gateway uses on the +proxy→provider hop). See +[examples/openai-multi-provider-proxy.yaml](../examples/openai-multi-provider-proxy.yaml). + +```yaml +spec: + provider: + id: openai-provider + auth: { type: api-key, header: X-API-Key, value: } + additionalProviders: + - id: anthropic-provider + auth: { type: api-key, header: X-API-Key, value: } + - id: azure-openai-provider + auth: { type: api-key, header: X-API-Key, value: } + policies: + - name: openai-header-router # 1. select provider + params: { headerName: x-provider, defaultProvider: openai-provider, mappings: [...] } + - name: openai-to-anthropic # 2. translators, one per provider + params: { model: claude-sonnet-4-5-20250929, id: anthropic-provider } + - name: openai-to-azure-openai + params: { apiVersion: "2024-02-15-preview", model: gpt-4o, id: azure-openai-provider } +``` + +### How the control plane turns this into a running chain + +1. **Types** ([llmproxy_types.go](../../kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go)): + new `LLMProxyAdditionalProvider` struct (`id`, optional `as` logical upstream + name, optional `auth`). +2. **Validation** ([llm_validator.go](../gateway-controller/pkg/config/llm_validator.go)): + validates each additional provider's `id`/`as` format, **rejects duplicate + upstream names**, and validates upstream auth (`type: api-key` requires + `header` + `value`). +3. **Transformation** ([llm_transformer.go](../gateway-controller/pkg/utils/llm_transformer.go)): + emits an upstream-auth policy **per provider**, each guarded by an + `ExecutionCondition` derived from `selected_provider`: + - primary provider: runs when no selection exists **or** selection matches — + `!('selected_provider' in request.Metadata) || request.Metadata['selected_provider'] == ''` + - additional providers: run only when selection matches — + `'selected_provider' in request.Metadata && request.Metadata['selected_provider'] == ''` +4. **Operator deploy** ([llmproxy_controller.go](../../kubernetes/gateway-operator/internal/controller/llmproxy_controller.go) + + [management_upstream_auth_payload.go](../../kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go)): + resolves each provider's secret value (including `valueFrom` secret refs) and + flattens it into the management payload the gateway-controller consumes. + +--- + +## 6. Summary of the changes + +The whole multi-provider LLM routing capability was built up across three change +sets — the initial feature, a refinement pass, and the current hardening + +Gemini work. Taken together they touch every layer: the **policies** that run in +the gateway, the **control-plane types and validation**, the **transformer** that +generates the policy chain, the **operator** that resolves credentials and +deploys, and the **examples** that show operators how to use it. Here is the +combined picture of what landed and why. + +**Policies (gateway data plane).** Four routing/translation policies were added +first — `openai-header-router` (provider selection) plus the +`openai-to-anthropic`, `openai-to-azure-openai`, and `openai-to-mistral` +translators — and the current work adds the `openai-to-gemini` translator, +completing the provider set. The router publishes the chosen provider into +`metadata["selected_provider"]`; each translator gates itself on that value so +the same chain works in both single-provider mode (no selection → the one +translator always runs) and multi-provider mode (runs only when its `id` +matches). + +**Control-plane types & validation.** `additionalProviders` became a +first-class concept: the operator CRD gained the `LLMProxyAdditionalProvider` +type (`id`, optional `as` logical upstream name, optional `auth`) along with the +generated deepcopy and CRD schema, and the management API / platform-api models +and services were extended to carry it through LLM deployment translation. New +validation in `llm_validator.go` enforces `id`/`as` formats, **rejects duplicate +upstream names**, and requires `header` + `value` for `api-key` upstream auth on +both the primary provider and every additional provider. + +**Transformer (chain generation).** `llm_transformer.go` now emits an +upstream-auth policy **per provider**, each guarded by an `ExecutionCondition` +derived from `selected_provider` — the primary provider runs when no selection +exists *or* the selection matches it, while additional providers run only on an +exact match. This is what makes per-provider credentials inject correctly when +one endpoint fans out to many backends. Covered by `llm_transformer_test.go`. + +**Operator (credential resolution & deploy).** The controller resolves each +provider's secret value — including `valueFrom` secret references — and flattens +it into the management payload the gateway-controller consumes, now for the +primary provider *and* each additional provider. Covered by the controller +tests. + +**Examples & supporting policies.** The provider/proxy example manifests +(`*-provider.yaml`, `*-proxy.yaml`) were added and then refined, with +`openai-multi-provider-proxy.yaml` reworked into the canonical end-to-end +demonstration of the feature. The `slugify-body` and `analytics` policy +definitions were adjusted along the way to stay compatible with the updated +proxy chain. + +--- + +## 7. What can you use it for? + +- **Vendor-agnostic LLM access** — clients code once against OpenAI, you swap + backends behind the proxy. +- **Header-based routing / A-B testing** — route `x-provider: anthropic` vs + `x-provider: openai` from the same endpoint. +- **Cost / capability routing** — send cheap traffic to one provider, premium + to another. +- **Centralized credential & policy enforcement** — keys, rate limits, + analytics, and auth live on the gateway, not in clients. +- **Migration & failover staging** — bring a new provider online as an + `additionalProvider` and shift traffic gradually via the router mappings. + +--- + +## 8. Modes at a glance + +| Mode | Router present? | `selected_provider` | Translator behavior | +|------------------|-----------------|---------------------|-------------------------------------| +| Single-provider | No | empty | The one translator always runs. | +| Multi-provider | Yes | set by router | Each runs only if its `id` matches. | diff --git a/gateway/dev-policies/openai-header-router/go.mod b/gateway/dev-policies/openai-header-router/go.mod new file mode 100644 index 0000000000..ad0b64d3e7 --- /dev/null +++ b/gateway/dev-policies/openai-header-router/go.mod @@ -0,0 +1,5 @@ +module github.com/wso2/api-platform/gateway/dev-policies/openai-header-router + +go 1.26.1 + +require github.com/wso2/api-platform/sdk/core v0.2.4 diff --git a/gateway/dev-policies/openai-header-router/go.sum b/gateway/dev-policies/openai-header-router/go.sum new file mode 100644 index 0000000000..f8667c54c7 --- /dev/null +++ b/gateway/dev-policies/openai-header-router/go.sum @@ -0,0 +1,2 @@ +github.com/wso2/api-platform/sdk/core v0.2.4 h1:dwwe7QROmf1HIeCqhfiv82/34n6oAe2b0hazYljpPS0= +github.com/wso2/api-platform/sdk/core v0.2.4/go.mod h1:vgNVzR16g9k5cun3VXZ7wDg8UGbPxsVU2TW8EbRCv0o= diff --git a/gateway/dev-policies/openai-header-router/openaiheaderrouter.go b/gateway/dev-policies/openai-header-router/openaiheaderrouter.go new file mode 100644 index 0000000000..405ed54232 --- /dev/null +++ b/gateway/dev-policies/openai-header-router/openaiheaderrouter.go @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package openaiheaderrouter + +import ( + "context" + "fmt" + "log/slog" + "strings" + + policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2" +) + +const ( + PolicyName = "openai-header-router" + + // DefaultHeaderName is the request header consulted when the operator + // does not override "headerName" in the policy parameters. + DefaultHeaderName = "x-provider" + + // MetadataKeySelectedProvider is the SharedContext.Metadata key the + // router writes after picking a provider. Downstream consumer + // policies read this key to decide whether to run. + MetadataKeySelectedProvider = "selected_provider" +) + +// HeaderMapping is a single header-value → provider-id rule. +type HeaderMapping struct { + // HeaderValue is matched against the incoming header value + // case-insensitively after trimming whitespace. + HeaderValue string + // Provider is the id published when this mapping wins. + Provider string +} + +// PolicyParams is the parsed, validated configuration for a single +// instantiation of the policy. +type PolicyParams struct { + // HeaderName is the request header read for selection. Comparison + // against incoming headers is case-insensitive. + HeaderName string + + // DefaultProvider is selected when the header is missing, empty, or + // no mapping matches. parseParams enforces it is non-empty. + DefaultProvider string + + // Mappings is checked in order; the first match wins. Order is the + // order the operator declared in the policy YAML. + Mappings []HeaderMapping +} + +// RouterPolicy is the Policy implementation registered with the gateway +// runtime. It is stateless across requests — selection depends only on the +// incoming header and the immutable params. +type RouterPolicy struct { + params PolicyParams +} + +// GetPolicy is the v1alpha2 factory entry point invoked by the gateway +// kernel for each policy instantiation on a route. +func GetPolicy( + _ policy.PolicyMetadata, + rawParams map[string]interface{}, +) (policy.Policy, error) { + parsed, err := parseParams(rawParams) + if err != nil { + return nil, fmt.Errorf("%s: invalid params: %w", PolicyName, err) + } + return &RouterPolicy{params: parsed}, nil +} + +// Mode declares the phases this policy participates in. Selection runs in the +// request-header phase so header-phase consumers observe the choice — notably +// the proxy->provider upstream auth injection (a set-headers policy gated on +// selected_provider), which executes in the header phase and would otherwise +// evaluate its gate before the selection exists. The body phase repeats the +// publish as an idempotent fallback; both phases share one *SharedContext, so +// a value written in the header phase is visible to body-phase consumers +// (e.g. the openai-to-* translators). +func (p *RouterPolicy) Mode() policy.ProcessingMode { + return policy.ProcessingMode{ + RequestHeaderMode: policy.HeaderModeProcess, + RequestBodyMode: policy.BodyModeBuffer, + ResponseHeaderMode: policy.HeaderModeSkip, + ResponseBodyMode: policy.BodyModeSkip, + } +} + +// OnRequestHeaders performs provider selection during the request-header phase +// so the selection is published before header-phase consumers (upstream auth +// injection) evaluate their selected_provider gate. +func (p *RouterPolicy) OnRequestHeaders( + _ context.Context, + reqCtx *policy.RequestHeaderContext, + _ map[string]interface{}, +) policy.RequestHeaderAction { + if reqCtx.SharedContext == nil { + return policy.UpstreamRequestHeaderModifications{} + } + if reqCtx.Metadata == nil { + reqCtx.Metadata = map[string]interface{}{} + } + p.publishSelection(reqCtx.Metadata, reqCtx.Headers) + return policy.UpstreamRequestHeaderModifications{} +} + +// OnRequestBody republishes the selection in the body phase as an idempotent +// fallback (e.g. if an upstream policy cleared metadata between phases). If +// selected_provider is already set, publishSelection leaves it untouched. +func (p *RouterPolicy) OnRequestBody( + _ context.Context, + reqCtx *policy.RequestContext, + _ map[string]interface{}, +) policy.RequestAction { + // SharedContext is required to publish the selection downstream. + if reqCtx.SharedContext == nil { + return policy.UpstreamRequestModifications{} + } + if reqCtx.Metadata == nil { + reqCtx.Metadata = map[string]interface{}{} + } + p.publishSelection(reqCtx.Metadata, reqCtx.Headers) + return policy.UpstreamRequestModifications{} +} + +// publishSelection reads the configured header, picks the provider via the +// mappings (falling back to defaultProvider), and writes the result into +// metadata. If selected_provider is already set by an upstream policy or an +// earlier phase, it is left untouched. Shared by the header and body phases. +func (p *RouterPolicy) publishSelection(metadata map[string]interface{}, headers *policy.Headers) { + if existing, ok := metadata[MetadataKeySelectedProvider].(string); ok && existing != "" { + return + } + + headerValue := readHeader(headers, p.params.HeaderName) + provider, source := p.selectProvider(headerValue) + metadata[MetadataKeySelectedProvider] = provider + + slog.Debug(PolicyName+": provider selected", + "headerName", p.params.HeaderName, "headerValue", headerValue, + "provider", provider, "source", source) +} + +// selectProvider picks a provider id for the given header value and +// returns both the chosen provider and a short tag describing why it was +// chosen (used for telemetry / logging). +// +// - "header" — a mapping matched the header value. +// - "default" — fell back to defaultProvider (header missing, empty, or +// no mapping matched). +func (p *RouterPolicy) selectProvider(headerValue string) (string, string) { + if headerValue != "" { + for _, m := range p.params.Mappings { + if strings.EqualFold(m.HeaderValue, headerValue) { + return m.Provider, "header" + } + } + } + return p.params.DefaultProvider, "default" +} + +// readHeader extracts the configured header from the request, trimmed of +// surrounding whitespace. Returns "" when the header is missing. +func readHeader(headers *policy.Headers, name string) string { + if headers == nil { + return "" + } + values := headers.Get(name) + if len(values) == 0 { + return "" + } + return strings.TrimSpace(values[0]) +} + +// parseParams validates and copies the raw parameter map into PolicyParams. +// All shape and value validation lives here so OnRequestBody can assume +// the configuration is sound. +func parseParams(params map[string]interface{}) (PolicyParams, error) { + result := PolicyParams{ + HeaderName: DefaultHeaderName, + } + + if v, err := optionalString(params, "headerName"); err != nil { + return result, err + } else if v != "" { + result.HeaderName = v + } + + defaultProvider, err := optionalString(params, "defaultProvider") + if err != nil { + return result, err + } + if defaultProvider == "" { + return result, fmt.Errorf("'defaultProvider' is required") + } + result.DefaultProvider = defaultProvider + + rawMappings, ok := params["mappings"] + if !ok || rawMappings == nil { + return result, fmt.Errorf("'mappings' is required") + } + list, ok := rawMappings.([]interface{}) + if !ok { + return result, fmt.Errorf("'mappings' must be an array") + } + if len(list) == 0 { + return result, fmt.Errorf("'mappings' must contain at least one entry") + } + + seenHeaderValues := map[string]struct{}{} + for i, raw := range list { + entry, ok := raw.(map[string]interface{}) + if !ok { + return result, fmt.Errorf("mappings[%d] must be an object", i) + } + headerValue, err := requiredEntryString(entry, "headerValue", i) + if err != nil { + return result, err + } + provider, err := requiredEntryString(entry, "provider", i) + if err != nil { + return result, err + } + + // Reject duplicate header values up-front so operators get a + // clear error instead of a silently-shadowed mapping. + key := strings.ToLower(headerValue) + if _, dup := seenHeaderValues[key]; dup { + return result, fmt.Errorf("mappings[%d].headerValue %q is duplicated (case-insensitive)", i, headerValue) + } + seenHeaderValues[key] = struct{}{} + + result.Mappings = append(result.Mappings, HeaderMapping{ + HeaderValue: headerValue, + Provider: provider, + }) + } + + return result, nil +} + +// optionalString reads a string parameter, trims it, and returns "" when +// the key is absent or its value is null. It rejects non-string values. +func optionalString(params map[string]interface{}, key string) (string, error) { + raw, ok := params[key] + if !ok || raw == nil { + return "", nil + } + v, ok := raw.(string) + if !ok { + return "", fmt.Errorf("'%s' must be a string", key) + } + return strings.TrimSpace(v), nil +} + +// requiredEntryString reads a required string field from a mappings entry, +// trimming whitespace and producing a structured error message that points +// at the offending array index. +func requiredEntryString(entry map[string]interface{}, key string, idx int) (string, error) { + raw, ok := entry[key] + if !ok || raw == nil { + return "", fmt.Errorf("mappings[%d].%s is required", idx, key) + } + v, ok := raw.(string) + if !ok { + return "", fmt.Errorf("mappings[%d].%s must be a string", idx, key) + } + v = strings.TrimSpace(v) + if v == "" { + return "", fmt.Errorf("mappings[%d].%s must not be empty", idx, key) + } + return v, nil +} diff --git a/gateway/dev-policies/openai-header-router/policy-definition.yaml b/gateway/dev-policies/openai-header-router/policy-definition.yaml new file mode 100644 index 0000000000..1d43bf3b10 --- /dev/null +++ b/gateway/dev-policies/openai-header-router/policy-definition.yaml @@ -0,0 +1,60 @@ +name: openai-header-router +version: v1.0.0 +description: | + Selects an LLM provider for OpenAI Chat Completions requests using a request + header, then publishes the selected provider id to shared metadata for + downstream translator policies. + +parameters: + type: object + additionalProperties: false + properties: + headerName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the request header to read for provider selection. + Comparison is case-insensitive. + default: x-provider + minLength: 1 + defaultProvider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id selected when the header is missing, empty, or does + not match any entry in "mappings". This must be a provider id that + a downstream translator is bound to. + minLength: 1 + mappings: + type: array + x-wso2-policy-advanced-param: false + description: | + Mapping from incoming header values to provider ids. The first + matching entry wins. + items: + type: object + additionalProperties: false + properties: + headerValue: + type: string + description: | + Header value to match (case-insensitive, leading/trailing + whitespace trimmed). + minLength: 1 + provider: + type: string + description: | + Provider id published to SharedContext.Metadata + ["selected_provider"] when this entry matches. + minLength: 1 + required: + - headerValue + - provider + minItems: 1 + required: + - mappings + - defaultProvider + +systemParameters: + type: object + properties: {} diff --git a/gateway/dev-policies/openai-to-anthropic/go.mod b/gateway/dev-policies/openai-to-anthropic/go.mod new file mode 100644 index 0000000000..79a4f1c83b --- /dev/null +++ b/gateway/dev-policies/openai-to-anthropic/go.mod @@ -0,0 +1,5 @@ +module github.com/wso2/api-platform/gateway/dev-policies/openai-to-anthropic + +go 1.26.1 + +require github.com/wso2/api-platform/sdk/core v0.2.4 diff --git a/gateway/dev-policies/openai-to-anthropic/go.sum b/gateway/dev-policies/openai-to-anthropic/go.sum new file mode 100644 index 0000000000..f8667c54c7 --- /dev/null +++ b/gateway/dev-policies/openai-to-anthropic/go.sum @@ -0,0 +1,2 @@ +github.com/wso2/api-platform/sdk/core v0.2.4 h1:dwwe7QROmf1HIeCqhfiv82/34n6oAe2b0hazYljpPS0= +github.com/wso2/api-platform/sdk/core v0.2.4/go.mod h1:vgNVzR16g9k5cun3VXZ7wDg8UGbPxsVU2TW8EbRCv0o= diff --git a/gateway/dev-policies/openai-to-anthropic/openaitoanthropic.go b/gateway/dev-policies/openai-to-anthropic/openaitoanthropic.go new file mode 100644 index 0000000000..9a18f0f8d4 --- /dev/null +++ b/gateway/dev-policies/openai-to-anthropic/openaitoanthropic.go @@ -0,0 +1,534 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package openaitoanthropic + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + + policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2" +) + +const ( + PolicyName = "openai-to-anthropic" + AnthropicMessagesPath = "/v1/messages" + DefaultAnthropicVersion = "2023-06-01" + DefaultMaxTokens = 4096 + MetadataKeySelectedProvider = "selected_provider" +) + +type PolicyParams struct { + Model string + AnthropicVersion string + Id string +} + +type TranslatorPolicy struct { + params PolicyParams +} + +func GetPolicy(_ policy.PolicyMetadata, rawParams map[string]interface{}) (policy.Policy, error) { + parsed, err := parseParams(rawParams) + if err != nil { + return nil, fmt.Errorf("%s: invalid params: %w", PolicyName, err) + } + return &TranslatorPolicy{params: parsed}, nil +} + +func (p *TranslatorPolicy) Mode() policy.ProcessingMode { + return policy.ProcessingMode{ + RequestHeaderMode: policy.HeaderModeSkip, + RequestBodyMode: policy.BodyModeBuffer, + ResponseHeaderMode: policy.HeaderModeSkip, + ResponseBodyMode: policy.BodyModeBuffer, + } +} + +func (p *TranslatorPolicy) OnRequestBody( + _ context.Context, + reqCtx *policy.RequestContext, + _ map[string]interface{}, +) policy.RequestAction { + if !p.shouldRun(reqCtx) { + return policy.UpstreamRequestModifications{} + } + + if reqCtx.Body == nil || !reqCtx.Body.Present || len(reqCtx.Body.Content) == 0 { + return errResponse(400, "Request body is empty.") + } + + var payload map[string]interface{} + if err := json.Unmarshal(reqCtx.Body.Content, &payload); err != nil { + return errResponse(400, fmt.Sprintf("Invalid JSON in request body: %s", err.Error())) + } + + model := p.params.Model + if model == "" { + return errResponse(400, "'model' policy parameter is required for Anthropic translation.") + } + + slog.Debug(PolicyName+": translating request", + "id", p.params.Id, "model", model, "path", AnthropicMessagesPath) + + mods := translateBody(payload, model, p.params) + if p.params.Id != "" && mods.UpstreamName == nil { + upstream := p.params.Id + mods.UpstreamName = &upstream + } + return mods +} + +// OnResponseBody translates the Anthropic Messages response into an OpenAI +// ChatCompletion-shaped body. Streaming SSE responses are passed through +// untouched — translating SSE requires a stateful chunk-level policy. +func (p *TranslatorPolicy) OnResponseBody( + _ context.Context, + respCtx *policy.ResponseContext, + _ map[string]interface{}, +) policy.ResponseAction { + if !p.shouldRunResponse(respCtx) { + return policy.DownstreamResponseModifications{} + } + + if respCtx.ResponseBody == nil || !respCtx.ResponseBody.Present || len(respCtx.ResponseBody.Content) == 0 { + return policy.DownstreamResponseModifications{} + } + + body := respCtx.ResponseBody.Content + if looksLikeSSE(body) { + slog.Debug(PolicyName+": SSE response passthrough", "status", respCtx.ResponseStatus) + return policy.DownstreamResponseModifications{} + } + + slog.Debug(PolicyName+": translating response", "status", respCtx.ResponseStatus) + return translateResponse(body, respCtx.ResponseStatus, p.params.Model) +} + +func (p *TranslatorPolicy) shouldRun(reqCtx *policy.RequestContext) bool { + return p.shouldRunForSelected(selectedProviderFromMetadata(reqCtx.SharedContext, reqCtx.Metadata)) +} + +func (p *TranslatorPolicy) shouldRunResponse(respCtx *policy.ResponseContext) bool { + return p.shouldRunForSelected(selectedProviderFromMetadata(respCtx.SharedContext, respCtx.Metadata)) +} + +func (p *TranslatorPolicy) shouldRunForSelected(selected string) bool { + if selected == "" { + // Single-provider mode: no router selected a provider, so run. + return true + } + return strings.EqualFold(selected, p.params.Id) +} + +func selectedProviderFromMetadata(shared *policy.SharedContext, metadata map[string]interface{}) string { + if shared == nil || metadata == nil { + return "" + } + rawSelectedProvider, hasSelectedProvider := metadata[MetadataKeySelectedProvider] + if !hasSelectedProvider { + return "" + } + selectedProvider, isString := rawSelectedProvider.(string) + if !isString { + return "" + } + return strings.TrimSpace(selectedProvider) +} + +func parseParams(params map[string]interface{}) (PolicyParams, error) { + result := PolicyParams{AnthropicVersion: DefaultAnthropicVersion} + + model, err := optionalString(params, "model") + if err != nil { + return result, err + } + if model == "" { + return result, fmt.Errorf("'model' is required") + } + result.Model = model + + if id, err := optionalString(params, "id"); err != nil { + return result, err + } else { + result.Id = id + } + + if anthropicVersion, err := optionalString(params, "anthropicVersion"); err != nil { + return result, err + } else if anthropicVersion != "" { + result.AnthropicVersion = anthropicVersion + } + + return result, nil +} + +func optionalString(params map[string]interface{}, key string) (string, error) { + rawValue, hasValue := params[key] + if !hasValue || rawValue == nil { + return "", nil + } + value, isString := rawValue.(string) + if !isString { + return "", fmt.Errorf("'%s' must be a string", key) + } + return strings.TrimSpace(value), nil +} + +func errResponse(statusCode int, message string) policy.ImmediateResponse { + body, _ := json.Marshal(map[string]string{"error": message}) + return policy.ImmediateResponse{ + StatusCode: statusCode, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: body, + } +} + +// translateBody converts an OpenAI Chat Completions request payload into an +// Anthropic Messages request and returns the upstream modifications. +func translateBody( + payload map[string]interface{}, model string, params PolicyParams, +) policy.UpstreamRequestModifications { + anthropicBody := map[string]interface{}{"model": model} + + if messages, hasMessages := payload["messages"].([]interface{}); hasMessages { + systemText, anthropicMessages := convertMessages(messages) + if systemText != "" { + anthropicBody["system"] = systemText + } + if len(anthropicMessages) > 0 { + anthropicBody["messages"] = anthropicMessages + } + } + + // Anthropic requires max_tokens; OpenAI's newer max_completion_tokens wins + // over legacy max_tokens, matching OpenAI's own precedence. + if maxCompletionTokens, hasMaxCompletionTokens := payload["max_completion_tokens"]; hasMaxCompletionTokens { + anthropicBody["max_tokens"] = maxCompletionTokens + } else if maxTokens, hasMaxTokens := payload["max_tokens"]; hasMaxTokens { + anthropicBody["max_tokens"] = maxTokens + } else { + anthropicBody["max_tokens"] = DefaultMaxTokens + } + + if temperature, hasTemperature := payload["temperature"]; hasTemperature { + anthropicBody["temperature"] = temperature + } + if topP, hasTopP := payload["top_p"]; hasTopP { + anthropicBody["top_p"] = topP + } + if stream, hasStream := payload["stream"]; hasStream { + anthropicBody["stream"] = stream + } + + if stop, hasStop := payload["stop"]; hasStop && stop != nil { + switch stopValue := stop.(type) { + case string: + anthropicBody["stop_sequences"] = []string{stopValue} + case []interface{}: + anthropicBody["stop_sequences"] = stopValue + } + } + + if tools, hasTools := payload["tools"].([]interface{}); hasTools && len(tools) > 0 { + if anthropicTools := convertTools(tools); len(anthropicTools) > 0 { + anthropicBody["tools"] = anthropicTools + } + } + + // "none" has no Anthropic equivalent — drop tools entirely so the model + // cannot call any. + if toolChoice, hasToolChoice := payload["tool_choice"]; hasToolChoice && toolChoice != nil { + if anthropicToolChoice := convertToolChoice(toolChoice); anthropicToolChoice == nil { + delete(anthropicBody, "tools") + } else { + anthropicBody["tool_choice"] = anthropicToolChoice + } + } + + newBody, err := json.Marshal(anthropicBody) + if err != nil { + return policy.UpstreamRequestModifications{ + Body: []byte(fmt.Sprintf(`{"error":"failed to marshal Anthropic body: %s"}`, err.Error())), + } + } + + newPath := AnthropicMessagesPath + return policy.UpstreamRequestModifications{ + Body: newBody, + Path: &newPath, + HeadersToSet: map[string]string{ + "content-type": "application/json", + "anthropic-version": params.AnthropicVersion, + }, + } +} + +// convertMessages extracts system text and rewrites messages into Anthropic +// role/content blocks. Consecutive tool messages collapse into one user +// message of tool_result blocks (Anthropic groups them together). +func convertMessages(messages []interface{}) (string, []map[string]interface{}) { + var systemParts []string + var anthropicMessages []map[string]interface{} + + for messageIndex := 0; messageIndex < len(messages); messageIndex++ { + message, isObject := messages[messageIndex].(map[string]interface{}) + if !isObject { + continue + } + role, _ := message["role"].(string) + + switch role { + case "system", "developer": + if text := extractTextContent(message["content"]); text != "" { + systemParts = append(systemParts, text) + } + case "user": + anthropicMessages = append(anthropicMessages, map[string]interface{}{ + "role": "user", + "content": convertUserContent(message["content"]), + }) + case "assistant": + anthropicMessages = append(anthropicMessages, map[string]interface{}{ + "role": "assistant", + "content": convertAssistantContent(message), + }) + case "tool": + var toolResults []interface{} + for messageIndex < len(messages) { + toolMessage, isObject := messages[messageIndex].(map[string]interface{}) + if !isObject { + break + } + if toolMessageRole, _ := toolMessage["role"].(string); toolMessageRole != "tool" { + break + } + toolCallID, _ := toolMessage["tool_call_id"].(string) + toolResults = append(toolResults, map[string]interface{}{ + "type": "tool_result", + "tool_use_id": toolCallID, + "content": extractTextContent(toolMessage["content"]), + }) + messageIndex++ + } + // Outer loop will increment; rewind so we revisit the terminator. + messageIndex-- + anthropicMessages = append(anthropicMessages, map[string]interface{}{ + "role": "user", + "content": toolResults, + }) + } + } + + return strings.Join(systemParts, "\n"), anthropicMessages +} + +func extractTextContent(content interface{}) string { + switch typedContent := content.(type) { + case string: + return typedContent + case []interface{}: + var parts []string + for _, contentPart := range typedContent { + contentBlock, isObject := contentPart.(map[string]interface{}) + if !isObject || contentBlock["type"] != "text" { + continue + } + if text, isString := contentBlock["text"].(string); isString { + parts = append(parts, text) + } + } + return strings.Join(parts, "") + } + return "" +} + +func convertUserContent(content interface{}) interface{} { + switch typedContent := content.(type) { + case string: + return typedContent + case []interface{}: + var blocks []interface{} + for _, contentPart := range typedContent { + contentBlock, isObject := contentPart.(map[string]interface{}) + if !isObject { + continue + } + switch contentBlock["type"].(string) { + case "text": + blocks = append(blocks, map[string]interface{}{ + "type": "text", + "text": contentBlock["text"], + }) + case "image_url": + if block := convertImage(contentBlock); block != nil { + blocks = append(blocks, block) + } + } + } + if len(blocks) > 0 { + return blocks + } + return content + } + return content +} + +func convertAssistantContent(message map[string]interface{}) interface{} { + toolCalls, hasToolCalls := message["tool_calls"].([]interface{}) + textContent := extractTextContent(message["content"]) + + if !hasToolCalls || len(toolCalls) == 0 { + if textContent != "" { + return textContent + } + return message["content"] + } + + var blocks []interface{} + if textContent != "" { + blocks = append(blocks, map[string]interface{}{ + "type": "text", + "text": textContent, + }) + } + + for _, rawToolCall := range toolCalls { + toolCall, isObject := rawToolCall.(map[string]interface{}) + if !isObject { + continue + } + id, _ := toolCall["id"].(string) + functionDefinition, _ := toolCall["function"].(map[string]interface{}) + if functionDefinition == nil { + continue + } + name, _ := functionDefinition["name"].(string) + argumentsString, _ := functionDefinition["arguments"].(string) + + // OpenAI wire format is a JSON string; Anthropic wants the decoded + // object inline. Fall back to empty object on parse failure so the + // request still validates. + var input interface{} + if argumentsString != "" { + if err := json.Unmarshal([]byte(argumentsString), &input); err != nil { + input = map[string]interface{}{} + } + } else { + input = map[string]interface{}{} + } + blocks = append(blocks, map[string]interface{}{ + "type": "tool_use", + "id": id, + "name": name, + "input": input, + }) + } + + return blocks +} + +func convertImage(contentBlock map[string]interface{}) map[string]interface{} { + imageObject, isObject := contentBlock["image_url"].(map[string]interface{}) + if !isObject { + return nil + } + imageURL, isString := imageObject["url"].(string) + if !isString || imageURL == "" { + return nil + } + + if strings.HasPrefix(imageURL, "data:") { + parts := strings.SplitN(imageURL, ",", 2) + if len(parts) != 2 { + return nil + } + meta := strings.TrimPrefix(parts[0], "data:") + metaParts := strings.SplitN(meta, ";", 2) + return map[string]interface{}{ + "type": "image", + "source": map[string]interface{}{ + "type": "base64", + "media_type": metaParts[0], + "data": parts[1], + }, + } + } + + return map[string]interface{}{ + "type": "image", + "source": map[string]interface{}{ + "type": "url", + "url": imageURL, + }, + } +} + +func convertTools(tools []interface{}) []map[string]interface{} { + var result []map[string]interface{} + for _, rawTool := range tools { + tool, isObject := rawTool.(map[string]interface{}) + if !isObject { + continue + } + functionDefinition, isObject := tool["function"].(map[string]interface{}) + if !isObject { + continue + } + anthropicTool := map[string]interface{}{"name": functionDefinition["name"]} + if description, hasDescription := functionDefinition["description"]; hasDescription { + anthropicTool["description"] = description + } + if parameters, hasParameters := functionDefinition["parameters"]; hasParameters { + anthropicTool["input_schema"] = parameters + } else { + anthropicTool["input_schema"] = map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + } + } + result = append(result, anthropicTool) + } + return result +} + +// convertToolChoice returns nil for "none" — the caller drops tools[] entirely +// since Anthropic has no negative tool_choice form. +func convertToolChoice(toolChoice interface{}) interface{} { + switch typedToolChoice := toolChoice.(type) { + case string: + switch typedToolChoice { + case "auto": + return map[string]interface{}{"type": "auto"} + case "none": + return nil + case "required": + return map[string]interface{}{"type": "any"} + } + case map[string]interface{}: + if functionDefinition, hasFunction := typedToolChoice["function"].(map[string]interface{}); hasFunction { + if name, hasName := functionDefinition["name"].(string); hasName { + return map[string]interface{}{"type": "tool", "name": name} + } + } + } + return map[string]interface{}{"type": "auto"} +} diff --git a/gateway/dev-policies/openai-to-anthropic/policy-definition.yaml b/gateway/dev-policies/openai-to-anthropic/policy-definition.yaml new file mode 100644 index 0000000000..abccca175e --- /dev/null +++ b/gateway/dev-policies/openai-to-anthropic/policy-definition.yaml @@ -0,0 +1,44 @@ +name: openai-to-anthropic +version: v1.0.0 +description: | + Translates OpenAI Chat Completions requests to Anthropic Messages format and + rewrites non-streaming Anthropic responses back to the OpenAI response shape. + Supports routed and unconditional execution modes. + +parameters: + type: object + additionalProperties: false + properties: + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Anthropic model name used in the translated request (for example + claude-sonnet-4-20250514). Overrides the OpenAI "model" field. + minLength: 1 + id: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider this translator targets. Used as the upstream cluster name + (matching an id/as in the LlmProxy additionalProviders list) and, in + multi-provider mode, as the key matched (case-insensitive) against + SharedContext.Metadata["selected_provider"]. When no provider has been + selected upstream (single-provider proxy), the translator always runs; + when a provider has been selected, it runs only if the selection + matches this id. When omitted, routing is left to the route's default + upstream. + minLength: 1 + anthropicVersion: + type: string + x-wso2-policy-advanced-param: true + description: | + Value of the anthropic-version request header sent upstream. + default: "2023-06-01" + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/gateway/dev-policies/openai-to-anthropic/response.go b/gateway/dev-policies/openai-to-anthropic/response.go new file mode 100644 index 0000000000..e42b45c6a1 --- /dev/null +++ b/gateway/dev-policies/openai-to-anthropic/response.go @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package openaitoanthropic + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2" +) + +const ( + anthropicStopEndTurn = "end_turn" + anthropicStopMaxTokens = "max_tokens" + anthropicStopStopSequence = "stop_sequence" + anthropicStopToolUse = "tool_use" + + openaiFinishStop = "stop" + openaiFinishLength = "length" + openaiFinishToolCalls = "tool_calls" +) + +type anthropicMessage struct { + ID string `json:"id"` + Type string `json:"type"` + Role string `json:"role"` + Model string `json:"model"` + Content []anthropicContentBlock `json:"content"` + StopReason string `json:"stop_reason"` + StopSequence *string `json:"stop_sequence,omitempty"` + Usage anthropicUsage `json:"usage"` +} + +type anthropicContentBlock struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Input any `json:"input,omitempty"` +} + +type anthropicUsage struct { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + CacheReadInputTokens int `json:"cache_read_input_tokens,omitempty"` + CacheCreationInputTokens int `json:"cache_creation_input_tokens,omitempty"` +} + +type anthropicErrorBody struct { + Type string `json:"type"` + Error struct { + Type string `json:"type"` + Message string `json:"message"` + } `json:"error"` +} + +func translateResponse(respBody []byte, status int, requestModel string) policy.ResponseAction { + if status >= 200 && status < 300 { + return translateSuccessResponse(respBody, requestModel) + } + return translateErrorResponse(respBody, status) +} + +func translateSuccessResponse(respBody []byte, requestModel string) policy.ResponseAction { + var msg anthropicMessage + if err := json.Unmarshal(respBody, &msg); err != nil { + // Upstream returned 2xx but body isn't Anthropic JSON — forward it + // unchanged rather than masking the payload behind a 500. + return policy.DownstreamResponseModifications{} + } + + responseModel := msg.Model + if responseModel == "" { + responseModel = requestModel + } + + openAIResp := buildOpenAIChatCompletion(&msg, responseModel) + newBody, err := json.Marshal(openAIResp) + if err != nil { + return errResponse(500, "failed to translate Anthropic response: "+err.Error()) + } + + return policy.DownstreamResponseModifications{ + Body: newBody, + HeadersToSet: map[string]string{ + "content-type": "application/json", + "content-length": fmt.Sprintf("%d", len(newBody)), + }, + } +} + +func translateErrorResponse(respBody []byte, status int) policy.ResponseAction { + openaiErr := map[string]interface{}{ + "error": map[string]interface{}{ + "type": mapStatusToOpenAIErrorType(status), + "message": string(respBody), + "code": fmt.Sprintf("%d", status), + }, + } + + var anthropicErr anthropicErrorBody + if err := json.Unmarshal(respBody, &anthropicErr); err == nil && anthropicErr.Error.Message != "" { + errMap := openaiErr["error"].(map[string]interface{}) + if anthropicErr.Error.Type != "" { + errMap["type"] = anthropicErr.Error.Type + } + errMap["message"] = anthropicErr.Error.Message + } + + newBody, err := json.Marshal(openaiErr) + if err != nil { + return errResponse(500, "failed to translate Anthropic error: "+err.Error()) + } + + return policy.DownstreamResponseModifications{ + Body: newBody, + HeadersToSet: map[string]string{ + "content-type": "application/json", + "content-length": fmt.Sprintf("%d", len(newBody)), + }, + } +} + +func buildOpenAIChatCompletion(msg *anthropicMessage, responseModel string) map[string]interface{} { + var textParts []string + var toolCalls []map[string]interface{} + + for i := range msg.Content { + block := &msg.Content[i] + switch block.Type { + case "text": + if block.Text != "" { + textParts = append(textParts, block.Text) + } + case "tool_use": + toolCalls = append(toolCalls, buildOpenAIToolCall(block)) + } + } + + message := map[string]interface{}{ + "role": "assistant", + "content": strings.Join(textParts, ""), + } + if len(toolCalls) > 0 { + message["tool_calls"] = toolCalls + } + + choice := map[string]interface{}{ + "index": 0, + "message": message, + "finish_reason": stopReasonToFinish(msg.StopReason, len(toolCalls) > 0), + } + + usage := map[string]interface{}{ + "prompt_tokens": msg.Usage.InputTokens, + "completion_tokens": msg.Usage.OutputTokens, + "total_tokens": msg.Usage.InputTokens + msg.Usage.OutputTokens, + } + if msg.Usage.CacheReadInputTokens > 0 || msg.Usage.CacheCreationInputTokens > 0 { + usage["prompt_tokens_details"] = map[string]interface{}{ + "cached_tokens": msg.Usage.CacheReadInputTokens, + "cache_creation_tokens": msg.Usage.CacheCreationInputTokens, + } + } + + id := msg.ID + if id == "" { + id = newChatCompletionID() + } else { + // Rewrite Anthropic "msg_*" id to OpenAI "chatcmpl-*" so prefix-aware + // clients still match. + id = "chatcmpl-" + strings.TrimPrefix(id, "msg_") + } + + return map[string]interface{}{ + "id": id, + "object": "chat.completion", + "created": time.Now().Unix(), + "model": responseModel, + "choices": []map[string]interface{}{choice}, + "usage": usage, + } +} + +func buildOpenAIToolCall(block *anthropicContentBlock) map[string]interface{} { + argsBytes, err := json.Marshal(block.Input) + argsStr := string(argsBytes) + if err != nil || block.Input == nil { + argsStr = "{}" + } + return map[string]interface{}{ + "id": block.ID, + "type": "function", + "function": map[string]interface{}{ + "name": block.Name, + "arguments": argsStr, + }, + } +} + +func stopReasonToFinish(stopReason string, hasToolCalls bool) string { + if hasToolCalls { + return openaiFinishToolCalls + } + switch stopReason { + case anthropicStopMaxTokens: + return openaiFinishLength + case anthropicStopToolUse: + return openaiFinishToolCalls + case anthropicStopEndTurn, anthropicStopStopSequence: + return openaiFinishStop + default: + return openaiFinishStop + } +} + +func mapStatusToOpenAIErrorType(status int) string { + switch { + case status == 400: + return "invalid_request_error" + case status == 401: + return "authentication_error" + case status == 403: + return "permission_error" + case status == 404: + return "not_found_error" + case status == 413: + return "request_too_large" + case status == 429: + return "rate_limit_error" + case status >= 500: + return "server_error" + default: + return "api_error" + } +} + +func newChatCompletionID() string { + var b [12]byte + if _, err := rand.Read(b[:]); err != nil { + return fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()) + } + return "chatcmpl-" + hex.EncodeToString(b[:]) +} + +func looksLikeSSE(body []byte) bool { + trimmed := strings.TrimLeft(string(body), " \r\n\t") + return strings.HasPrefix(trimmed, "event:") || strings.HasPrefix(trimmed, "data:") +} diff --git a/gateway/dev-policies/openai-to-azure-openai/HOWTO.md b/gateway/dev-policies/openai-to-azure-openai/HOWTO.md new file mode 100644 index 0000000000..eca3085d33 --- /dev/null +++ b/gateway/dev-policies/openai-to-azure-openai/HOWTO.md @@ -0,0 +1,212 @@ +# How to Run API Platform with the OpenAI Translator Policy + +This guide walks you through building and running the full API Platform +gateway with the `openai-to-azure-openai` translator policy enabled. + +--- + +## Prerequisites + +| Tool | Minimum version | +|-----------------|-----------------| +| Go | 1.26+ | +| Docker / Docker Compose | Latest | +| Make | 3.x+ | + +--- + +## 1. Verify the policy is registered in `build.yaml` + +The policy should already be listed in `gateway/build.yaml`: + +```yaml +policies: + # ... other policies ... + - name: openai-to-azure-openai + filePath: ./dev-policies/openai-to-azure-openai +``` + +If it is missing, add the entry above at the end of the `policies` list. + +--- + +## 2. Copy the policy definition to default-policies + +The gateway controller needs the policy schema at startup: + +```bash +cp gateway/dev-policies/openai-to-azure-openai/policy-definition.yaml \ + gateway/gateway-controller/default-policies/openai-to-azure-openai.yaml +``` + +--- + +## 3. Build the gateway images + +From the `gateway/` directory: + +```bash +cd gateway +make build # builds gateway-runtime, gateway-builder, and gateway-controller +``` + +This compiles the policy into the gateway-runtime binary and generates the +Docker images: + +- `ghcr.io/wso2/api-platform/gateway-runtime:` +- `ghcr.io/wso2/api-platform/gateway-controller:` + +--- + +## 4. Start the platform + +```bash +cd gateway +docker compose up -d +``` + +The platform exposes: + +| Service | Port | Description | +|----------------------|-------|------------------------------| +| Gateway HTTP | 8080 | API ingress (HTTP) | +| Gateway HTTPS | 8443 | API ingress (HTTPS) | +| Controller REST API | 9090 | Management REST API | +| Controller Admin | 9094 | Admin API | +| Envoy Admin | 9901 | Envoy admin dashboard | +| Sample Backend | 15000 | Echo / mock backend | + +--- + +## 5. Configure the policy on an API route + +Use the gateway controller REST API to attach the policy to a route. + +### Azure OpenAI example + +```json +{ + "policies": [ + { + "name": "openai-to-azure-openai", + "parameters": { + "targetProvider": "azure-openai", + "apiVersion": "2024-02-15-preview", + "model": "gpt-4o" + } + } + ] +} +``` + +### Anthropic example + +```json +{ + "policies": [ + { + "name": "openai-to-azure-openai", + "parameters": { + "targetProvider": "anthropic", + "model": "claude-sonnet-4-20250514" + } + } + ] +} +``` + +### Mistral example + +```json +{ + "policies": [ + { + "name": "openai-to-azure-openai", + "parameters": { + "targetProvider": "mistral", + "model": "mistral-large-latest" + } + } + ] +} +``` + +--- + +## 6. Test with curl + +Send an OpenAI-format request to the gateway and verify the translation: + +```bash +# Azure OpenAI +curl -s http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o", + "messages": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ], + "max_tokens": 256 + }' +``` + +```bash +# Anthropic (same OpenAI body — policy translates automatically) +curl -s http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "claude-sonnet-4-20250514", + "messages": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ], + "max_tokens": 256 + }' +``` + +--- + +## 7. What the policy translates (body only) + +| Source (OpenAI) | Azure OpenAI | Anthropic | Mistral | +|------------------------------|-------------------------|---------------------------------|----------------------------| +| `model` | deployment id in URL | `model` | `model` | +| `messages[role=system]` | passthrough | top-level `system` field | passthrough | +| `messages` | passthrough | Anthropic message blocks | passthrough | +| `max_tokens` | passthrough | `max_tokens` (required) | passthrough | +| `stop` | passthrough | `stop_sequences` | passthrough | +| `tools` | passthrough | `name`/`description`/`input_schema` | passthrough | +| `tool_choice` | passthrough | `{"type":"auto"/"any"/"tool"}` | passthrough | +| `tool_calls` (assistant) | passthrough | `tool_use` content blocks | passthrough | +| `role=tool` messages | passthrough | `tool_result` in user messages | passthrough | +| `image_url` content parts | passthrough | Anthropic `image` source blocks | passthrough | +| Path | `/openai/deployments/…` | `/v1/messages` | `/v1/chat/completions` | +| Unsupported fields | — | stripped | stripped (`logprobs`, etc.) | + +--- + +## 8. Cleaning up (before committing) + +Per `dev-policies/README.md`, before committing remove the `filePath` entry +from `gateway/build.yaml` and the copied YAML from `default-policies/` so +CI builds are unaffected: + +```bash +rm gateway/gateway-controller/default-policies/openai-to-azure-openai.yaml +# Edit gateway/build.yaml and remove the openai-to-azure-openai entry +``` + +--- + +## Troubleshooting + +- **Policy not found at runtime** — make sure `build.yaml` lists the policy + *and* `make build` was re-run after adding it. +- **Azure returns 404** — verify `apiVersion` matches a valid Azure API + version and the model/deployment name is correct. +- **Anthropic returns 401** — the policy does NOT set the `x-api-key` header. + Use a `set-headers` policy (or route-level config) to inject the Anthropic + API key. +- **Mistral returns 422** — check that the model name is valid for your + Mistral subscription. diff --git a/gateway/dev-policies/openai-to-azure-openai/go.mod b/gateway/dev-policies/openai-to-azure-openai/go.mod new file mode 100644 index 0000000000..1ab502881a --- /dev/null +++ b/gateway/dev-policies/openai-to-azure-openai/go.mod @@ -0,0 +1,5 @@ +module github.com/wso2/api-platform/gateway/dev-policies/openai-to-azure-openai + +go 1.26.1 + +require github.com/wso2/api-platform/sdk/core v0.2.4 diff --git a/gateway/dev-policies/openai-to-azure-openai/go.sum b/gateway/dev-policies/openai-to-azure-openai/go.sum new file mode 100644 index 0000000000..f8667c54c7 --- /dev/null +++ b/gateway/dev-policies/openai-to-azure-openai/go.sum @@ -0,0 +1,2 @@ +github.com/wso2/api-platform/sdk/core v0.2.4 h1:dwwe7QROmf1HIeCqhfiv82/34n6oAe2b0hazYljpPS0= +github.com/wso2/api-platform/sdk/core v0.2.4/go.mod h1:vgNVzR16g9k5cun3VXZ7wDg8UGbPxsVU2TW8EbRCv0o= diff --git a/gateway/dev-policies/openai-to-azure-openai/openaitoazureopenai.go b/gateway/dev-policies/openai-to-azure-openai/openaitoazureopenai.go new file mode 100644 index 0000000000..eebddf2c4a --- /dev/null +++ b/gateway/dev-policies/openai-to-azure-openai/openaitoazureopenai.go @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Package openaitoazureopenai rewrites an OpenAI Chat Completions request path +// into the Azure OpenAI form +// /openai/deployments/{deployment}/{pathSuffix}?api-version=. +// The body passes through unchanged; Azure already emits OpenAI-shaped +// responses, so response translation is intentionally not implemented. +package openaitoazureopenai + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + + policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2" +) + +const ( + PolicyName = "openai-to-azure-openai" + DefaultPathSuffix = "/chat/completions" + MetadataKeySelectedProvider = "selected_provider" +) + +type PolicyParams struct { + APIVersion string + Model string + PathSuffix string + // Id is the upstream provider this translator targets. It is both the + // upstream cluster the request is routed to and the key matched + // (case-insensitive) against SharedContext.Metadata["selected_provider"] + // in multi-provider mode. + Id string +} + +type TranslatorPolicy struct { + params PolicyParams +} + +func GetPolicy(_ policy.PolicyMetadata, rawParams map[string]interface{}) (policy.Policy, error) { + parsed, err := parseParams(rawParams) + if err != nil { + return nil, fmt.Errorf("%s: invalid params: %w", PolicyName, err) + } + return &TranslatorPolicy{params: parsed}, nil +} + +// Mode buffers the request body even though we don't modify it — the +// deployment id may have to be read from the body's "model" field when the +// operator hasn't pinned one. +func (p *TranslatorPolicy) Mode() policy.ProcessingMode { + return policy.ProcessingMode{ + RequestHeaderMode: policy.HeaderModeSkip, + RequestBodyMode: policy.BodyModeBuffer, + ResponseHeaderMode: policy.HeaderModeSkip, + ResponseBodyMode: policy.BodyModeSkip, + } +} + +func (p *TranslatorPolicy) OnRequestBody( + _ context.Context, + reqCtx *policy.RequestContext, + _ map[string]interface{}, +) policy.RequestAction { + if !p.shouldRun(reqCtx) { + return policy.UpstreamRequestModifications{} + } + + deployment := p.params.Model + if deployment == "" { + deployment = readModelFromBody(reqCtx) + } + if deployment == "" { + return errResponse(400, + "'model' is required in the request body (or as a policy parameter) "+ + "to derive the Azure deployment id.") + } + + newPath := buildAzurePath(deployment, p.params.PathSuffix, p.params.APIVersion) + slog.Debug(PolicyName+": rewriting request path", + "id", p.params.Id, "deployment", deployment, "path", newPath) + + mods := policy.UpstreamRequestModifications{Path: &newPath} + if p.params.Id != "" { + upstream := p.params.Id + mods.UpstreamName = &upstream + } + return mods +} + +// shouldRun reports whether the request should be rewritten. When no upstream +// router (e.g. openai-header-router) has published a selected provider into the +// metadata, the proxy is in single-provider mode and the translator always +// runs. When a provider has been selected, the translator runs only if that +// selection matches its own "id". +func (p *TranslatorPolicy) shouldRun(reqCtx *policy.RequestContext) bool { + selected := selectedProvider(reqCtx) + if selected == "" { + // Single-provider mode: no router selected a provider, so run. + return true + } + return strings.EqualFold(selected, p.params.Id) +} + +func selectedProvider(reqCtx *policy.RequestContext) string { + if reqCtx == nil || reqCtx.SharedContext == nil || reqCtx.Metadata == nil { + return "" + } + raw, ok := reqCtx.Metadata[MetadataKeySelectedProvider] + if !ok { + return "" + } + v, ok := raw.(string) + if !ok { + return "" + } + return strings.TrimSpace(v) +} + +func readModelFromBody(reqCtx *policy.RequestContext) string { + if reqCtx.Body == nil || !reqCtx.Body.Present || len(reqCtx.Body.Content) == 0 { + return "" + } + var payload map[string]interface{} + if err := json.Unmarshal(reqCtx.Body.Content, &payload); err != nil { + return "" + } + model, _ := payload["model"].(string) + return strings.TrimSpace(model) +} + +func buildAzurePath(deployment, pathSuffix, apiVersion string) string { + return fmt.Sprintf("/openai/deployments/%s%s?api-version=%s", + deployment, pathSuffix, apiVersion) +} + +func parseParams(params map[string]interface{}) (PolicyParams, error) { + result := PolicyParams{PathSuffix: DefaultPathSuffix} + + apiVersion, err := optionalString(params, "apiVersion") + if err != nil { + return result, err + } + if apiVersion == "" { + return result, fmt.Errorf("'apiVersion' is required") + } + result.APIVersion = apiVersion + + if v, err := optionalString(params, "model"); err != nil { + return result, err + } else { + result.Model = v + } + + if v, err := optionalString(params, "pathSuffix"); err != nil { + return result, err + } else if v != "" { + // PathSuffix must start with '/' so buildAzurePath can concatenate + // without inspecting the operator-supplied string. + if !strings.HasPrefix(v, "/") { + v = "/" + v + } + result.PathSuffix = v + } + + if v, err := optionalString(params, "id"); err != nil { + return result, err + } else { + result.Id = v + } + + return result, nil +} + +func optionalString(params map[string]interface{}, key string) (string, error) { + raw, ok := params[key] + if !ok || raw == nil { + return "", nil + } + v, ok := raw.(string) + if !ok { + return "", fmt.Errorf("'%s' must be a string", key) + } + return strings.TrimSpace(v), nil +} + +func errResponse(statusCode int, message string) policy.ImmediateResponse { + body, _ := json.Marshal(map[string]string{"error": message}) + return policy.ImmediateResponse{ + StatusCode: statusCode, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: body, + } +} diff --git a/gateway/dev-policies/openai-to-azure-openai/policy-definition.yaml b/gateway/dev-policies/openai-to-azure-openai/policy-definition.yaml new file mode 100644 index 0000000000..5e1ebe7b51 --- /dev/null +++ b/gateway/dev-policies/openai-to-azure-openai/policy-definition.yaml @@ -0,0 +1,53 @@ +name: openai-to-azure-openai +version: v1.0.0 +description: | + Routes OpenAI Chat Completions requests to an Azure OpenAI upstream by + rewriting the request path with deployment and API version details. The body + passes through unchanged. + +parameters: + type: object + additionalProperties: false + properties: + apiVersion: + type: string + x-wso2-policy-advanced-param: false + description: | + Azure OpenAI api-version query-string value (for example + 2024-02-15-preview). Required because Azure rejects requests + without it. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Azure deployment id used in the rewritten path. When omitted, the + request body's "model" field is used as the deployment id. + minLength: 1 + id: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider this translator targets. Used as the upstream cluster name + (matching an id/as in the LlmProxy additionalProviders list) and, in + multi-provider mode, as the key matched (case-insensitive) against + SharedContext.Metadata["selected_provider"]. When no provider has been + selected upstream (single-provider proxy), the translator always runs; + when a provider has been selected, it runs only if the selection + matches this id. When omitted, routing is left to the route's default + upstream. + minLength: 1 + pathSuffix: + type: string + x-wso2-policy-advanced-param: true + description: | + Endpoint suffix appended after the deployment segment in the + rewritten path. Defaults to /chat/completions. + default: /chat/completions + minLength: 1 + required: + - apiVersion + +systemParameters: + type: object + properties: {} diff --git a/gateway/dev-policies/openai-to-gemini/go.mod b/gateway/dev-policies/openai-to-gemini/go.mod new file mode 100644 index 0000000000..5421001ebf --- /dev/null +++ b/gateway/dev-policies/openai-to-gemini/go.mod @@ -0,0 +1,5 @@ +module github.com/wso2/api-platform/gateway/dev-policies/openai-to-gemini + +go 1.26.1 + +require github.com/wso2/api-platform/sdk/core v0.2.4 diff --git a/gateway/dev-policies/openai-to-gemini/go.sum b/gateway/dev-policies/openai-to-gemini/go.sum new file mode 100644 index 0000000000..f8667c54c7 --- /dev/null +++ b/gateway/dev-policies/openai-to-gemini/go.sum @@ -0,0 +1,2 @@ +github.com/wso2/api-platform/sdk/core v0.2.4 h1:dwwe7QROmf1HIeCqhfiv82/34n6oAe2b0hazYljpPS0= +github.com/wso2/api-platform/sdk/core v0.2.4/go.mod h1:vgNVzR16g9k5cun3VXZ7wDg8UGbPxsVU2TW8EbRCv0o= diff --git a/gateway/dev-policies/openai-to-gemini/openaitogemini.go b/gateway/dev-policies/openai-to-gemini/openaitogemini.go new file mode 100644 index 0000000000..9091381ddb --- /dev/null +++ b/gateway/dev-policies/openai-to-gemini/openaitogemini.go @@ -0,0 +1,606 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Package openaitogemini translates OpenAI Chat Completions requests/responses +// to and from the Google Gemini generateContent API. +package openaitogemini + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + + policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2" +) + +const ( + PolicyName = "openai-to-gemini" + DefaultAPIVersion = "v1beta" + MethodGenerateContent = "generateContent" + MethodStreamGenerateContent = "streamGenerateContent" + MetadataKeySelectedProvider = "selected_provider" +) + +type PolicyParams struct { + Model string + APIVersion string + // Id is the upstream provider this translator targets. It is both the + // upstream cluster the request is routed to and the key matched + // (case-insensitive) against SharedContext.Metadata["selected_provider"] + // in multi-provider mode. + Id string +} + +type TranslatorPolicy struct { + params PolicyParams +} + +func GetPolicy(_ policy.PolicyMetadata, rawParams map[string]interface{}) (policy.Policy, error) { + parsed, err := parseParams(rawParams) + if err != nil { + return nil, fmt.Errorf("%s: invalid params: %w", PolicyName, err) + } + return &TranslatorPolicy{params: parsed}, nil +} + +func (p *TranslatorPolicy) Mode() policy.ProcessingMode { + return policy.ProcessingMode{ + RequestHeaderMode: policy.HeaderModeSkip, + RequestBodyMode: policy.BodyModeBuffer, + ResponseHeaderMode: policy.HeaderModeSkip, + ResponseBodyMode: policy.BodyModeBuffer, + } +} + +func (p *TranslatorPolicy) OnRequestBody( + _ context.Context, + reqCtx *policy.RequestContext, + _ map[string]interface{}, +) policy.RequestAction { + if !p.shouldRun(reqCtx) { + return policy.UpstreamRequestModifications{} + } + + if reqCtx.Body == nil || !reqCtx.Body.Present || len(reqCtx.Body.Content) == 0 { + return errResponse(400, "Request body is empty.") + } + + var payload map[string]interface{} + if err := json.Unmarshal(reqCtx.Body.Content, &payload); err != nil { + return errResponse(400, fmt.Sprintf("Invalid JSON in request body: %s", err.Error())) + } + + model := p.params.Model + if model == "" { + return errResponse(400, "'model' policy parameter is required for Gemini translation.") + } + + stream := false + if v, ok := payload["stream"].(bool); ok { + stream = v + } + + mods := translateBody(payload, model, p.params, stream) + if p.params.Id != "" && mods.UpstreamName == nil { + upstream := p.params.Id + mods.UpstreamName = &upstream + } + + slog.Debug(PolicyName+": translating request", + "id", p.params.Id, "model", model, "stream", stream) + return mods +} + +// OnResponseBody translates the Gemini response into an OpenAI ChatCompletion +// body. Streaming SSE responses are passed through untouched. +func (p *TranslatorPolicy) OnResponseBody( + _ context.Context, + respCtx *policy.ResponseContext, + _ map[string]interface{}, +) policy.ResponseAction { + if !p.shouldRunResponse(respCtx) { + return policy.DownstreamResponseModifications{} + } + + if respCtx.ResponseBody == nil || !respCtx.ResponseBody.Present || len(respCtx.ResponseBody.Content) == 0 { + return policy.DownstreamResponseModifications{} + } + + body := respCtx.ResponseBody.Content + if looksLikeSSE(body) { + slog.Debug(PolicyName+": SSE response passthrough", "status", respCtx.ResponseStatus) + return policy.DownstreamResponseModifications{} + } + + slog.Debug(PolicyName+": translating response", "status", respCtx.ResponseStatus) + return translateResponse(body, respCtx.ResponseStatus, p.params.Model) +} + +// shouldRun reports whether the request should be translated. When no upstream +// router (e.g. openai-header-router) has published a selected provider into the +// metadata, the proxy is in single-provider mode and the translator always +// runs. When a provider has been selected, the translator runs only if that +// selection matches its own "id". +func (p *TranslatorPolicy) shouldRun(reqCtx *policy.RequestContext) bool { + return p.shouldRunForSelected(selectedProviderFromMetadata(reqCtx.SharedContext, reqCtx.Metadata)) +} + +func (p *TranslatorPolicy) shouldRunResponse(respCtx *policy.ResponseContext) bool { + return p.shouldRunForSelected(selectedProviderFromMetadata(respCtx.SharedContext, respCtx.Metadata)) +} + +func (p *TranslatorPolicy) shouldRunForSelected(selected string) bool { + if selected == "" { + // Single-provider mode: no router selected a provider, so run. + return true + } + return strings.EqualFold(selected, p.params.Id) +} + +func selectedProviderFromMetadata(shared *policy.SharedContext, metadata map[string]interface{}) string { + if shared == nil || metadata == nil { + return "" + } + raw, ok := metadata[MetadataKeySelectedProvider] + if !ok { + return "" + } + v, ok := raw.(string) + if !ok { + return "" + } + return strings.TrimSpace(v) +} + +func parseParams(params map[string]interface{}) (PolicyParams, error) { + result := PolicyParams{APIVersion: DefaultAPIVersion} + + model, err := optionalString(params, "model") + if err != nil { + return result, err + } + if model == "" { + return result, fmt.Errorf("'model' is required") + } + result.Model = model + + if v, err := optionalString(params, "id"); err != nil { + return result, err + } else { + result.Id = v + } + + if v, err := optionalString(params, "apiVersion"); err != nil { + return result, err + } else if v != "" { + result.APIVersion = v + } + + return result, nil +} + +func optionalString(params map[string]interface{}, key string) (string, error) { + raw, ok := params[key] + if !ok || raw == nil { + return "", nil + } + v, ok := raw.(string) + if !ok { + return "", fmt.Errorf("'%s' must be a string", key) + } + return strings.TrimSpace(v), nil +} + +func errResponse(statusCode int, message string) policy.ImmediateResponse { + body, _ := json.Marshal(map[string]string{"error": message}) + return policy.ImmediateResponse{ + StatusCode: statusCode, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: body, + } +} + +// translateBody converts an OpenAI Chat Completions payload into a Gemini +// generateContent request and returns the upstream modifications. +func translateBody( + payload map[string]interface{}, model string, params PolicyParams, stream bool, +) policy.UpstreamRequestModifications { + geminiBody := map[string]interface{}{} + + if msgs, ok := payload["messages"].([]interface{}); ok { + systemParts, contents := convertMessages(msgs) + if len(systemParts) > 0 { + geminiBody["systemInstruction"] = map[string]interface{}{"parts": systemParts} + } + if len(contents) > 0 { + geminiBody["contents"] = contents + } + } + + if cfg := buildGenerationConfig(payload); len(cfg) > 0 { + geminiBody["generationConfig"] = cfg + } + + if tools, ok := payload["tools"].([]interface{}); ok && len(tools) > 0 { + if geminiTools := convertTools(tools); len(geminiTools) > 0 { + geminiBody["tools"] = geminiTools + } + } + + // "none" has no Gemini equivalent in the tool list; drop tools entirely + // and set functionCallingConfig.mode=NONE. + if tc, ok := payload["tool_choice"]; ok && tc != nil { + toolConfig, drop := convertToolChoice(tc) + if drop { + delete(geminiBody, "tools") + } + if toolConfig != nil { + geminiBody["toolConfig"] = toolConfig + } + } + + newBody, err := json.Marshal(geminiBody) + if err != nil { + return policy.UpstreamRequestModifications{ + Body: []byte(fmt.Sprintf(`{"error":"failed to marshal Gemini body: %s"}`, err.Error())), + } + } + + newPath := buildGeminiPath(params.APIVersion, model, stream) + return policy.UpstreamRequestModifications{ + Body: newBody, + Path: &newPath, + HeadersToSet: map[string]string{ + "content-type": "application/json", + }, + } +} + +func buildGeminiPath(apiVersion, model string, stream bool) string { + method := MethodGenerateContent + suffix := "" + if stream { + method = MethodStreamGenerateContent + suffix = "?alt=sse" + } + return fmt.Sprintf("/%s/models/%s:%s%s", apiVersion, model, method, suffix) +} + +// convertMessages walks OpenAI messages[] and produces Gemini systemInstruction +// parts and contents[]. Consecutive user/tool parts flush into one Gemini +// "user" content; assistant messages flush as a "model" content. +func convertMessages(msgs []interface{}) ([]interface{}, []interface{}) { + var systemParts []interface{} + var contents []interface{} + var pendingUserParts []interface{} + + flushUser := func() { + if len(pendingUserParts) == 0 { + return + } + contents = append(contents, map[string]interface{}{ + "role": "user", + "parts": pendingUserParts, + }) + pendingUserParts = nil + } + + for _, raw := range msgs { + msg, ok := raw.(map[string]interface{}) + if !ok { + continue + } + role, _ := msg["role"].(string) + + switch role { + case "system", "developer": + if parts := convertTextOrPartsToGemini(msg["content"]); len(parts) > 0 { + systemParts = append(systemParts, parts...) + } + case "user": + if parts := convertUserContent(msg["content"]); len(parts) > 0 { + pendingUserParts = append(pendingUserParts, parts...) + } + case "tool": + toolCallID, _ := msg["tool_call_id"].(string) + text := extractTextContent(msg["content"]) + pendingUserParts = append(pendingUserParts, map[string]interface{}{ + "functionResponse": map[string]interface{}{ + "name": toolCallID, + "response": map[string]interface{}{"output": text}, + }, + }) + case "assistant": + flushUser() + contents = append(contents, map[string]interface{}{ + "role": "model", + "parts": convertAssistantParts(msg), + }) + } + } + flushUser() + + return systemParts, contents +} + +func convertTextOrPartsToGemini(content interface{}) []interface{} { + switch c := content.(type) { + case string: + if c == "" { + return nil + } + return []interface{}{map[string]interface{}{"text": c}} + case []interface{}: + var parts []interface{} + for _, p := range c { + m, ok := p.(map[string]interface{}) + if !ok || m["type"] != "text" { + continue + } + if text, ok := m["text"].(string); ok && text != "" { + parts = append(parts, map[string]interface{}{"text": text}) + } + } + return parts + } + return nil +} + +func convertUserContent(content interface{}) []interface{} { + switch c := content.(type) { + case string: + if c == "" { + return nil + } + return []interface{}{map[string]interface{}{"text": c}} + case []interface{}: + var parts []interface{} + for _, p := range c { + m, ok := p.(map[string]interface{}) + if !ok { + continue + } + switch m["type"].(string) { + case "text": + if text, ok := m["text"].(string); ok && text != "" { + parts = append(parts, map[string]interface{}{"text": text}) + } + case "image_url": + if part := convertImage(m); part != nil { + parts = append(parts, part) + } + } + } + return parts + } + return nil +} + +func extractTextContent(content interface{}) string { + switch c := content.(type) { + case string: + return c + case []interface{}: + var parts []string + for _, p := range c { + m, ok := p.(map[string]interface{}) + if !ok || m["type"] != "text" { + continue + } + if text, ok := m["text"].(string); ok { + parts = append(parts, text) + } + } + return strings.Join(parts, "") + } + return "" +} + +// convertAssistantParts builds the Gemini "model" parts list from an OpenAI +// assistant message: text becomes a text part; each tool_call becomes a +// functionCall part with arguments parsed back into an object. +func convertAssistantParts(msg map[string]interface{}) []interface{} { + var parts []interface{} + + if text := extractTextContent(msg["content"]); text != "" { + parts = append(parts, map[string]interface{}{"text": text}) + } + + toolCalls, _ := msg["tool_calls"].([]interface{}) + for _, tc := range toolCalls { + toolCall, ok := tc.(map[string]interface{}) + if !ok { + continue + } + fn, _ := toolCall["function"].(map[string]interface{}) + if fn == nil { + continue + } + name, _ := fn["name"].(string) + argsStr, _ := fn["arguments"].(string) + + // OpenAI wire format is a JSON string; Gemini wants an object. Fall + // back to empty object on parse failure so the request still validates. + var args map[string]interface{} + if argsStr != "" { + if err := json.Unmarshal([]byte(argsStr), &args); err != nil { + args = map[string]interface{}{} + } + } else { + args = map[string]interface{}{} + } + parts = append(parts, map[string]interface{}{ + "functionCall": map[string]interface{}{ + "name": name, + "args": args, + }, + }) + } + + return parts +} + +func convertImage(m map[string]interface{}) map[string]interface{} { + imgObj, ok := m["image_url"].(map[string]interface{}) + if !ok { + return nil + } + imageURL, ok := imgObj["url"].(string) + if !ok || imageURL == "" { + return nil + } + + if strings.HasPrefix(imageURL, "data:") { + comma := strings.SplitN(imageURL, ",", 2) + if len(comma) != 2 { + return nil + } + meta := strings.TrimPrefix(comma[0], "data:") + metaParts := strings.SplitN(meta, ";", 2) + return map[string]interface{}{ + "inlineData": map[string]interface{}{ + "mimeType": metaParts[0], + "data": comma[1], + }, + } + } + + // Remote URL — Gemini accepts fileData with file_uri. mimeType defaults + // to image/jpeg when the URL has no recognizable extension. + mimeType := "image/jpeg" + lower := strings.ToLower(imageURL) + switch { + case strings.HasSuffix(lower, ".png"): + mimeType = "image/png" + case strings.HasSuffix(lower, ".gif"): + mimeType = "image/gif" + case strings.HasSuffix(lower, ".webp"): + mimeType = "image/webp" + } + return map[string]interface{}{ + "fileData": map[string]interface{}{ + "mimeType": mimeType, + "fileUri": imageURL, + }, + } +} + +func buildGenerationConfig(payload map[string]interface{}) map[string]interface{} { + cfg := map[string]interface{}{} + + if v, ok := payload["temperature"]; ok { + cfg["temperature"] = v + } + if v, ok := payload["top_p"]; ok { + cfg["topP"] = v + } + if v, ok := payload["max_completion_tokens"]; ok { + cfg["maxOutputTokens"] = v + } else if v, ok := payload["max_tokens"]; ok { + cfg["maxOutputTokens"] = v + } + if v, ok := payload["stop"]; ok && v != nil { + switch s := v.(type) { + case string: + cfg["stopSequences"] = []string{s} + case []interface{}: + cfg["stopSequences"] = s + } + } + if v, ok := payload["n"]; ok { + cfg["candidateCount"] = v + } + if v, ok := payload["seed"]; ok { + cfg["seed"] = v + } + if v, ok := payload["frequency_penalty"]; ok { + cfg["frequencyPenalty"] = v + } + if v, ok := payload["presence_penalty"]; ok { + cfg["presencePenalty"] = v + } + + return cfg +} + +func convertTools(tools []interface{}) []map[string]interface{} { + var decls []map[string]interface{} + for _, t := range tools { + tool, ok := t.(map[string]interface{}) + if !ok { + continue + } + fn, ok := tool["function"].(map[string]interface{}) + if !ok { + continue + } + decl := map[string]interface{}{"name": fn["name"]} + if desc, ok := fn["description"]; ok { + decl["description"] = desc + } + if params, ok := fn["parameters"]; ok { + decl["parameters"] = params + } + decls = append(decls, decl) + } + if len(decls) == 0 { + return nil + } + return []map[string]interface{}{ + {"functionDeclarations": decls}, + } +} + +// convertToolChoice returns (toolConfig, dropTools). dropTools=true tells the +// caller to remove the tools[] field — used for the "none" choice where +// Gemini also expects mode=NONE. +func convertToolChoice(tc interface{}) (map[string]interface{}, bool) { + switch v := tc.(type) { + case string: + switch v { + case "auto": + return map[string]interface{}{ + "functionCallingConfig": map[string]interface{}{"mode": "AUTO"}, + }, false + case "required": + return map[string]interface{}{ + "functionCallingConfig": map[string]interface{}{"mode": "ANY"}, + }, false + case "none": + return map[string]interface{}{ + "functionCallingConfig": map[string]interface{}{"mode": "NONE"}, + }, true + } + case map[string]interface{}: + if fn, ok := v["function"].(map[string]interface{}); ok { + if name, ok := fn["name"].(string); ok { + return map[string]interface{}{ + "functionCallingConfig": map[string]interface{}{ + "mode": "ANY", + "allowedFunctionNames": []string{name}, + }, + }, false + } + } + } + return map[string]interface{}{ + "functionCallingConfig": map[string]interface{}{"mode": "AUTO"}, + }, false +} diff --git a/gateway/dev-policies/openai-to-gemini/policy-definition.yaml b/gateway/dev-policies/openai-to-gemini/policy-definition.yaml new file mode 100644 index 0000000000..c7166fbf91 --- /dev/null +++ b/gateway/dev-policies/openai-to-gemini/policy-definition.yaml @@ -0,0 +1,46 @@ +name: openai-to-gemini +version: v1.0.0 +description: | + Translates OpenAI Chat Completions requests to the Gemini generateContent API + format and rewrites non-streaming Gemini responses back to the OpenAI shape. + Supports routed and unconditional execution modes. + +parameters: + type: object + additionalProperties: false + properties: + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Gemini model name used in the translated request (for example + gemini-2.5-pro). Overrides the OpenAI "model" field and is used + in the rewritten path. + minLength: 1 + id: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider this translator targets. Used as the upstream cluster name + (matching an id/as in the LlmProxy additionalProviders list) and, in + multi-provider mode, as the key matched (case-insensitive) against + SharedContext.Metadata["selected_provider"]. When no provider has been + selected upstream (single-provider proxy), the translator always runs; + when a provider has been selected, it runs only if the selection + matches this id. When omitted, routing is left to the route's default + upstream. + minLength: 1 + apiVersion: + type: string + x-wso2-policy-advanced-param: true + description: | + Gemini API version segment used in the rewritten path + (/{apiVersion}/models/{model}:generateContent). + default: "v1beta" + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/gateway/dev-policies/openai-to-gemini/response.go b/gateway/dev-policies/openai-to-gemini/response.go new file mode 100644 index 0000000000..696c3fea69 --- /dev/null +++ b/gateway/dev-policies/openai-to-gemini/response.go @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package openaitogemini + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2" +) + +const ( + geminiFinishStop = "STOP" + geminiFinishMaxTokens = "MAX_TOKENS" + geminiFinishSafety = "SAFETY" + geminiFinishRecitation = "RECITATION" + geminiFinishOther = "OTHER" + geminiFinishBlocklist = "BLOCKLIST" + geminiFinishProhibited = "PROHIBITED_CONTENT" + geminiFinishSPII = "SPII" + geminiFinishMalfunction = "MALFORMED_FUNCTION_CALL" + geminiFinishImageRecit = "IMAGE_SAFETY" + geminiFinishLanguage = "LANGUAGE" + geminiFinishUnspecified = "FINISH_REASON_UNSPECIFIED" + + openaiFinishStop = "stop" + openaiFinishLength = "length" + openaiFinishToolCalls = "tool_calls" + openaiFinishContentFilter = "content_filter" +) + +type geminiResponse struct { + Candidates []geminiCandidate `json:"candidates"` + UsageMetadata *geminiUsageMetadata `json:"usageMetadata,omitempty"` + ModelVersion string `json:"modelVersion,omitempty"` + ResponseID string `json:"responseId,omitempty"` + PromptFeedback *geminiPromptFeedback `json:"promptFeedback,omitempty"` +} + +type geminiCandidate struct { + Content *geminiContent `json:"content,omitempty"` + FinishReason string `json:"finishReason,omitempty"` + Index int `json:"index,omitempty"` +} + +type geminiContent struct { + Role string `json:"role,omitempty"` + Parts []geminiPart `json:"parts,omitempty"` +} + +type geminiPart struct { + Text string `json:"text,omitempty"` + Thought bool `json:"thought,omitempty"` + FunctionCall *geminiFunctionCall `json:"functionCall,omitempty"` +} + +type geminiFunctionCall struct { + Name string `json:"name"` + Args map[string]interface{} `json:"args"` +} + +type geminiUsageMetadata struct { + PromptTokenCount int `json:"promptTokenCount"` + CandidatesTokenCount int `json:"candidatesTokenCount"` + TotalTokenCount int `json:"totalTokenCount"` + CachedContentTokenCount int `json:"cachedContentTokenCount,omitempty"` + ThoughtsTokenCount int `json:"thoughtsTokenCount,omitempty"` +} + +type geminiPromptFeedback struct { + BlockReason string `json:"blockReason,omitempty"` +} + +type geminiErrorBody struct { + Error struct { + Code int `json:"code"` + Message string `json:"message"` + Status string `json:"status"` + } `json:"error"` +} + +func translateResponse(respBody []byte, status int, requestModel string) policy.ResponseAction { + if status >= 200 && status < 300 { + return translateSuccessResponse(respBody, requestModel) + } + return translateErrorResponse(respBody, status) +} + +func translateSuccessResponse(respBody []byte, requestModel string) policy.ResponseAction { + var resp geminiResponse + if err := json.Unmarshal(respBody, &resp); err != nil { + // Upstream returned 2xx but body isn't Gemini JSON — forward it + // unchanged rather than masking the payload behind a 500. + return policy.DownstreamResponseModifications{} + } + + responseModel := resp.ModelVersion + if responseModel == "" { + responseModel = requestModel + } + + openAIResp := buildOpenAIChatCompletion(&resp, responseModel) + newBody, err := json.Marshal(openAIResp) + if err != nil { + return errResponse(500, "failed to translate Gemini response: "+err.Error()) + } + + return policy.DownstreamResponseModifications{ + Body: newBody, + HeadersToSet: map[string]string{ + "content-type": "application/json", + "content-length": fmt.Sprintf("%d", len(newBody)), + }, + } +} + +func translateErrorResponse(respBody []byte, status int) policy.ResponseAction { + openaiErr := map[string]interface{}{ + "error": map[string]interface{}{ + "type": mapStatusToOpenAIErrorType(status), + "message": string(respBody), + "code": fmt.Sprintf("%d", status), + }, + } + + var geminiErr geminiErrorBody + if err := json.Unmarshal(respBody, &geminiErr); err == nil && geminiErr.Error.Message != "" { + errMap := openaiErr["error"].(map[string]interface{}) + if geminiErr.Error.Status != "" { + errMap["type"] = geminiErr.Error.Status + } + errMap["message"] = geminiErr.Error.Message + } + + newBody, err := json.Marshal(openaiErr) + if err != nil { + return errResponse(500, "failed to translate Gemini error: "+err.Error()) + } + + return policy.DownstreamResponseModifications{ + Body: newBody, + HeadersToSet: map[string]string{ + "content-type": "application/json", + "content-length": fmt.Sprintf("%d", len(newBody)), + }, + } +} + +func buildOpenAIChatCompletion(resp *geminiResponse, responseModel string) map[string]interface{} { + choices := make([]map[string]interface{}, 0, len(resp.Candidates)) + for i, cand := range resp.Candidates { + choices = append(choices, buildOpenAIChoice(&cand, i)) + } + + usage := buildOpenAIUsage(resp.UsageMetadata) + + id := resp.ResponseID + if id == "" { + id = newChatCompletionID() + } else { + id = "chatcmpl-" + id + } + + return map[string]interface{}{ + "id": id, + "object": "chat.completion", + "created": time.Now().Unix(), + "model": responseModel, + "choices": choices, + "usage": usage, + } +} + +func buildOpenAIChoice(cand *geminiCandidate, defaultIndex int) map[string]interface{} { + var textParts []string + var toolCalls []map[string]interface{} + + if cand.Content != nil { + for i := range cand.Content.Parts { + part := &cand.Content.Parts[i] + switch { + case part.FunctionCall != nil: + toolCalls = append(toolCalls, buildOpenAIToolCall(part.FunctionCall)) + case part.Text != "" && !part.Thought: + textParts = append(textParts, part.Text) + } + } + } + + message := map[string]interface{}{ + "role": "assistant", + "content": strings.Join(textParts, ""), + } + if len(toolCalls) > 0 { + message["tool_calls"] = toolCalls + } + + idx := cand.Index + if idx == 0 && defaultIndex > 0 { + idx = defaultIndex + } + return map[string]interface{}{ + "index": idx, + "message": message, + "finish_reason": finishReasonToOpenAI(cand.FinishReason, len(toolCalls) > 0), + } +} + +func buildOpenAIToolCall(fc *geminiFunctionCall) map[string]interface{} { + argsBytes, err := json.Marshal(fc.Args) + argsStr := string(argsBytes) + if err != nil || fc.Args == nil { + argsStr = "{}" + } + return map[string]interface{}{ + "id": "call_" + shortRandomID(), + "type": "function", + "function": map[string]interface{}{ + "name": fc.Name, + "arguments": argsStr, + }, + } +} + +func buildOpenAIUsage(meta *geminiUsageMetadata) map[string]interface{} { + if meta == nil { + return map[string]interface{}{ + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + } + } + + completion := meta.CandidatesTokenCount + meta.ThoughtsTokenCount + usage := map[string]interface{}{ + "prompt_tokens": meta.PromptTokenCount, + "completion_tokens": completion, + "total_tokens": meta.TotalTokenCount, + } + if meta.CachedContentTokenCount > 0 { + usage["prompt_tokens_details"] = map[string]interface{}{ + "cached_tokens": meta.CachedContentTokenCount, + } + } + if meta.ThoughtsTokenCount > 0 { + usage["completion_tokens_details"] = map[string]interface{}{ + "reasoning_tokens": meta.ThoughtsTokenCount, + } + } + return usage +} + +func finishReasonToOpenAI(reason string, hasToolCalls bool) string { + if hasToolCalls { + return openaiFinishToolCalls + } + switch reason { + case geminiFinishStop, "": + return openaiFinishStop + case geminiFinishMaxTokens: + return openaiFinishLength + case geminiFinishSafety, geminiFinishRecitation, geminiFinishBlocklist, + geminiFinishProhibited, geminiFinishSPII, geminiFinishImageRecit, geminiFinishLanguage: + return openaiFinishContentFilter + default: + return openaiFinishStop + } +} + +func mapStatusToOpenAIErrorType(status int) string { + switch { + case status == 400: + return "invalid_request_error" + case status == 401: + return "authentication_error" + case status == 403: + return "permission_error" + case status == 404: + return "not_found_error" + case status == 413: + return "request_too_large" + case status == 429: + return "rate_limit_error" + case status >= 500: + return "server_error" + default: + return "api_error" + } +} + +func newChatCompletionID() string { + var b [12]byte + if _, err := rand.Read(b[:]); err != nil { + return fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()) + } + return "chatcmpl-" + hex.EncodeToString(b[:]) +} + +func shortRandomID() string { + var b [8]byte + if _, err := rand.Read(b[:]); err != nil { + return fmt.Sprintf("%d", time.Now().UnixNano()) + } + return hex.EncodeToString(b[:]) +} + +func looksLikeSSE(body []byte) bool { + trimmed := strings.TrimLeft(string(body), " \r\n\t") + return strings.HasPrefix(trimmed, "event:") || strings.HasPrefix(trimmed, "data:") +} diff --git a/gateway/dev-policies/openai-to-mistral/go.mod b/gateway/dev-policies/openai-to-mistral/go.mod new file mode 100644 index 0000000000..e68ae03b00 --- /dev/null +++ b/gateway/dev-policies/openai-to-mistral/go.mod @@ -0,0 +1,5 @@ +module github.com/wso2/api-platform/gateway/dev-policies/openai-to-mistral + +go 1.26.1 + +require github.com/wso2/api-platform/sdk/core v0.2.4 diff --git a/gateway/dev-policies/openai-to-mistral/go.sum b/gateway/dev-policies/openai-to-mistral/go.sum new file mode 100644 index 0000000000..f8667c54c7 --- /dev/null +++ b/gateway/dev-policies/openai-to-mistral/go.sum @@ -0,0 +1,2 @@ +github.com/wso2/api-platform/sdk/core v0.2.4 h1:dwwe7QROmf1HIeCqhfiv82/34n6oAe2b0hazYljpPS0= +github.com/wso2/api-platform/sdk/core v0.2.4/go.mod h1:vgNVzR16g9k5cun3VXZ7wDg8UGbPxsVU2TW8EbRCv0o= diff --git a/gateway/dev-policies/openai-to-mistral/openaitomistral.go b/gateway/dev-policies/openai-to-mistral/openaitomistral.go new file mode 100644 index 0000000000..c1cd5cebb2 --- /dev/null +++ b/gateway/dev-policies/openai-to-mistral/openaitomistral.go @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Package openaitomistral translates OpenAI Chat Completions requests for +// Mistral's OpenAI-compatible chat completions endpoint. +package openaitomistral + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + + policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2" +) + +const ( + PolicyName = "openai-to-mistral" + MistralChatCompletionsPath = "/v1/chat/completions" + MetadataKeySelectedProvider = "selected_provider" +) + +// unsupportedRequestFields lists OpenAI fields Mistral's API rejects. Keep in +// sync with https://docs.mistral.ai/api/. +var unsupportedRequestFields = []string{ + "logprobs", + "top_logprobs", + "logit_bias", + "n", + "service_tier", + "store", + "metadata", + "user", +} + +type PolicyParams struct { + // Model overrides the OpenAI "model" field in the translated request. + Model string + // Id is the upstream provider this translator targets. It serves two + // purposes: it is the upstream cluster the request is routed to, and it + // is the key matched (case-insensitive) against + // SharedContext.Metadata["selected_provider"] in multi-provider mode. + Id string +} + +type TranslatorPolicy struct { + params PolicyParams +} + +func GetPolicy(_ policy.PolicyMetadata, rawParams map[string]interface{}) (policy.Policy, error) { + parsed, err := parseParams(rawParams) + if err != nil { + return nil, fmt.Errorf("%s: invalid params: %w", PolicyName, err) + } + return &TranslatorPolicy{params: parsed}, nil +} + +func (p *TranslatorPolicy) Mode() policy.ProcessingMode { + return policy.ProcessingMode{ + RequestHeaderMode: policy.HeaderModeSkip, + RequestBodyMode: policy.BodyModeBuffer, + ResponseHeaderMode: policy.HeaderModeSkip, + ResponseBodyMode: policy.BodyModeBuffer, + } +} + +func (p *TranslatorPolicy) OnRequestBody( + _ context.Context, + reqCtx *policy.RequestContext, + _ map[string]interface{}, +) policy.RequestAction { + if !p.shouldRun(reqCtx) { + return policy.UpstreamRequestModifications{} + } + + if reqCtx.Body == nil || !reqCtx.Body.Present || len(reqCtx.Body.Content) == 0 { + return errResponse(400, "Request body is empty.") + } + + var payload map[string]interface{} + if err := json.Unmarshal(reqCtx.Body.Content, &payload); err != nil { + return errResponse(400, fmt.Sprintf("Invalid JSON in request body: %s", err.Error())) + } + + model := p.params.Model + if model == "" { + return errResponse(400, "'model' policy parameter is required for Mistral translation.") + } + + payload["model"] = model + for _, key := range unsupportedRequestFields { + delete(payload, key) + } + + newBody, err := json.Marshal(payload) + if err != nil { + return errResponse(500, "failed to marshal Mistral body: "+err.Error()) + } + + newPath := MistralChatCompletionsPath + slog.Debug(PolicyName+": translating request", + "id", p.params.Id, "model", model, "path", newPath) + + mods := policy.UpstreamRequestModifications{ + Body: newBody, + Path: &newPath, + HeadersToSet: map[string]string{"content-type": "application/json"}, + } + if p.params.Id != "" { + upstream := p.params.Id + mods.UpstreamName = &upstream + } + return mods +} + +// OnResponseBody normalises the Mistral response into OpenAI shape. Mistral +// already emits OpenAI-shaped success bodies, so the work here is mostly +// error-envelope translation and ensuring the response model is non-empty. +// SSE streaming bodies pass through untouched. +func (p *TranslatorPolicy) OnResponseBody( + _ context.Context, + respCtx *policy.ResponseContext, + _ map[string]interface{}, +) policy.ResponseAction { + if !p.shouldRunResponse(respCtx) { + return policy.DownstreamResponseModifications{} + } + + if respCtx.ResponseBody == nil || !respCtx.ResponseBody.Present || len(respCtx.ResponseBody.Content) == 0 { + return policy.DownstreamResponseModifications{} + } + + body := respCtx.ResponseBody.Content + if looksLikeSSE(body) { + slog.Debug(PolicyName+": SSE response passthrough", "status", respCtx.ResponseStatus) + return policy.DownstreamResponseModifications{} + } + + slog.Debug(PolicyName+": translating response", "status", respCtx.ResponseStatus) + return translateResponse(body, respCtx.ResponseStatus, p.params.Model) +} + +// shouldRun reports whether the request should be translated. When no upstream +// router (e.g. openai-header-router) has published a selected provider into the +// metadata, the proxy is in single-provider mode and the translator always +// runs. When a provider has been selected, the translator runs only if that +// selection matches its own "id". +func (p *TranslatorPolicy) shouldRun(reqCtx *policy.RequestContext) bool { + return p.shouldRunForSelected(selectedProviderFromMetadata(reqCtx.SharedContext, reqCtx.Metadata)) +} + +func (p *TranslatorPolicy) shouldRunResponse(respCtx *policy.ResponseContext) bool { + return p.shouldRunForSelected(selectedProviderFromMetadata(respCtx.SharedContext, respCtx.Metadata)) +} + +func (p *TranslatorPolicy) shouldRunForSelected(selected string) bool { + if selected == "" { + // Single-provider mode: no router selected a provider, so run. + return true + } + return strings.EqualFold(selected, p.params.Id) +} + +func selectedProviderFromMetadata(shared *policy.SharedContext, metadata map[string]interface{}) string { + if shared == nil || metadata == nil { + return "" + } + raw, ok := metadata[MetadataKeySelectedProvider] + if !ok { + return "" + } + v, ok := raw.(string) + if !ok { + return "" + } + return strings.TrimSpace(v) +} + +func parseParams(params map[string]interface{}) (PolicyParams, error) { + result := PolicyParams{} + + model, err := optionalString(params, "model") + if err != nil { + return result, err + } + if model == "" { + return result, fmt.Errorf("'model' is required") + } + result.Model = model + + if v, err := optionalString(params, "id"); err != nil { + return result, err + } else { + result.Id = v + } + + return result, nil +} + +func optionalString(params map[string]interface{}, key string) (string, error) { + raw, ok := params[key] + if !ok || raw == nil { + return "", nil + } + v, ok := raw.(string) + if !ok { + return "", fmt.Errorf("'%s' must be a string", key) + } + return strings.TrimSpace(v), nil +} + +func errResponse(statusCode int, message string) policy.ImmediateResponse { + body, _ := json.Marshal(map[string]string{"error": message}) + return policy.ImmediateResponse{ + StatusCode: statusCode, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: body, + } +} diff --git a/gateway/dev-policies/openai-to-mistral/policy-definition.yaml b/gateway/dev-policies/openai-to-mistral/policy-definition.yaml new file mode 100644 index 0000000000..6c23dc8941 --- /dev/null +++ b/gateway/dev-policies/openai-to-mistral/policy-definition.yaml @@ -0,0 +1,37 @@ +name: openai-to-mistral +version: v1.0.0 +description: | + Adapts OpenAI Chat Completions requests for Mistral-compatible upstreams by + pinning the model, stripping unsupported fields, and rewriting the request + path. Mistral-compatible responses are passed back in OpenAI shape. + +parameters: + type: object + additionalProperties: false + properties: + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Mistral model name used in the translated request (for example + mistral-large-latest). Overrides the OpenAI "model" field. + minLength: 1 + id: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider this translator targets. Used as the upstream cluster name + (matching an id/as in the LlmProxy additionalProviders list) and, in + multi-provider mode, as the key matched (case-insensitive) against + SharedContext.Metadata["selected_provider"]. When no provider has been + selected upstream (single-provider proxy), the translator always runs; + when a provider has been selected, it runs only if the selection + matches this id. When omitted, routing is left to the route's default + upstream. + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/gateway/dev-policies/openai-to-mistral/response.go b/gateway/dev-policies/openai-to-mistral/response.go new file mode 100644 index 0000000000..a9bf5df21f --- /dev/null +++ b/gateway/dev-policies/openai-to-mistral/response.go @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package openaitomistral + +import ( + "encoding/json" + "fmt" + "strings" + + policy "github.com/wso2/api-platform/sdk/core/policy/v1alpha2" +) + +// mistralErrorBody covers both shapes Mistral returns on failure: a flat +// {"message": "..."} envelope and the OpenAI-style {"error": {...}} envelope. +type mistralErrorBody struct { + Message string `json:"message"` + Type string `json:"type"` + Code string `json:"code"` + Error struct { + Type string `json:"type"` + Message string `json:"message"` + Code string `json:"code"` + } `json:"error"` +} + +func translateResponse(respBody []byte, status int, requestModel string) policy.ResponseAction { + if status >= 200 && status < 300 { + return translateSuccessResponse(respBody, requestModel) + } + return translateErrorResponse(respBody, status) +} + +// translateSuccessResponse leaves Mistral's OpenAI-shaped body alone but +// backfills the "model" field with the operator-pinned model when the +// upstream omitted it, so clients always see a non-empty model. +func translateSuccessResponse(respBody []byte, requestModel string) policy.ResponseAction { + var payload map[string]interface{} + if err := json.Unmarshal(respBody, &payload); err != nil { + return policy.DownstreamResponseModifications{} + } + + if m, _ := payload["model"].(string); strings.TrimSpace(m) == "" && requestModel != "" { + payload["model"] = requestModel + } + + newBody, err := json.Marshal(payload) + if err != nil { + return policy.DownstreamResponseModifications{} + } + + return policy.DownstreamResponseModifications{ + Body: newBody, + HeadersToSet: map[string]string{ + "content-type": "application/json", + "content-length": fmt.Sprintf("%d", len(newBody)), + }, + } +} + +// translateErrorResponse rewrites Mistral error envelopes into the OpenAI +// error envelope so clients see a single error shape regardless of upstream. +func translateErrorResponse(respBody []byte, status int) policy.ResponseAction { + errType := mapStatusToOpenAIErrorType(status) + errMessage := string(respBody) + errCode := fmt.Sprintf("%d", status) + + var parsed mistralErrorBody + if err := json.Unmarshal(respBody, &parsed); err == nil { + switch { + case parsed.Error.Message != "": + errMessage = parsed.Error.Message + if parsed.Error.Type != "" { + errType = parsed.Error.Type + } + if parsed.Error.Code != "" { + errCode = parsed.Error.Code + } + case parsed.Message != "": + errMessage = parsed.Message + if parsed.Type != "" { + errType = parsed.Type + } + if parsed.Code != "" { + errCode = parsed.Code + } + } + } + + openaiErr := map[string]interface{}{ + "error": map[string]interface{}{ + "type": errType, + "message": errMessage, + "code": errCode, + }, + } + + newBody, err := json.Marshal(openaiErr) + if err != nil { + return errResponse(500, "failed to translate Mistral error: "+err.Error()) + } + + return policy.DownstreamResponseModifications{ + Body: newBody, + HeadersToSet: map[string]string{ + "content-type": "application/json", + "content-length": fmt.Sprintf("%d", len(newBody)), + }, + } +} + +func mapStatusToOpenAIErrorType(status int) string { + switch { + case status == 400: + return "invalid_request_error" + case status == 401: + return "authentication_error" + case status == 403: + return "permission_error" + case status == 404: + return "not_found_error" + case status == 413: + return "request_too_large" + case status == 422: + return "invalid_request_error" + case status == 429: + return "rate_limit_error" + case status >= 500: + return "server_error" + default: + return "api_error" + } +} + +func looksLikeSSE(body []byte) bool { + trimmed := strings.TrimLeft(string(body), " \r\n\t") + return strings.HasPrefix(trimmed, "event:") || strings.HasPrefix(trimmed, "data:") +} diff --git a/gateway/dev-policies/openaidefinition.yaml b/gateway/dev-policies/openaidefinition.yaml new file mode 100644 index 0000000000..f22a32745b --- /dev/null +++ b/gateway/dev-policies/openaidefinition.yaml @@ -0,0 +1,22725 @@ +openapi: 3.0.0 +info: + title: OpenAI API + description: The OpenAI REST API. Please see https://platform.openai.com/docs/api-reference for more details. + version: 2.3.0 + termsOfService: https://openai.com/policies/terms-of-use + contact: + name: OpenAI Support + url: https://help.openai.com/ + license: + name: MIT + url: https://github.com/openai/openai-openapi/blob/master/LICENSE + x-marketplace-tags: + - name: AI + description: AI and LLM APIs +servers: + - url: https://api.openai.com/v1 +tags: + - name: Assistants + description: Build Assistants that can call models and use tools. + - name: Audio + description: Turn audio into text or text into audio. + - name: Chat + description: Given a list of messages comprising a conversation, the model will return a response. + - name: Completions + description: Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. + - name: Embeddings + description: Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. + - name: Fine-tuning + description: Manage fine-tuning jobs to tailor a model to your specific training data. + - name: Batch + description: Create large batches of API requests to run asynchronously. + - name: Files + description: Files are used to upload documents that can be used with features like Assistants and Fine-tuning. + - name: Uploads + description: Use Uploads to upload large files in multiple parts. + - name: Images + description: Given a prompt and/or an input image, the model will generate a new image. + - name: Models + description: List and describe the various models available in the API. + - name: Moderations + description: Given a input text, outputs if the model classifies it as potentially harmful. + - name: Audit Logs + description: List user actions and configuration changes within this organization. +paths: + /chat/completions: + post: + operationId: createChatCompletion + tags: + - Chat + - text-generation + summary: Creates a model response for the given chat conversation. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateChatCompletionRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateChatCompletionResponse' + x-oaiMeta: + name: Create chat completion + group: chat + returns: | + Returns a [chat completion](/docs/api-reference/chat/object) object, or a streamed sequence of [chat completion chunk](/docs/api-reference/chat/streaming) objects if the request is streamed. + path: create + examples: + - title: Default + request: + curl: | + curl https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "VAR_model_id", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello!" + } + ] + }' + python: | + from openai import OpenAI + client = OpenAI() + + completion = client.chat.completions.create( + model="VAR_model_id", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ] + ) + + print(completion.choices[0].message) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const completion = await openai.chat.completions.create({ + messages: [{ role: "system", content: "You are a helpful assistant." }], + model: "VAR_model_id", + }); + + console.log(completion.choices[0]); + } + + main(); + response: | + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4o-mini", + "system_fingerprint": "fp_44709d6fcb", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "\n\nHello there, how may I assist you today?", + }, + "logprobs": null, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + } + - title: Image input + request: + curl: | + curl https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-4o", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "What'\''s in this image?" + }, + { + "type": "image_url", + "image_url": { + "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + } + } + ] + } + ], + "max_tokens": 300 + }' + python: | + from openai import OpenAI + + client = OpenAI() + + response = client.chat.completions.create( + model="gpt-4o", + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + { + "type": "image_url", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + }, + ], + } + ], + max_tokens=300, + ) + + print(response.choices[0]) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const response = await openai.chat.completions.create({ + model: "gpt-4o", + messages: [ + { + role: "user", + content: [ + { type: "text", text: "What's in this image?" }, + { + type: "image_url", + image_url: + "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + }, + ], + }, + ], + }); + console.log(response.choices[0]); + } + main(); + response: | + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4o-mini", + "system_fingerprint": "fp_44709d6fcb", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "\n\nThis image shows a wooden boardwalk extending through a lush green marshland.", + }, + "logprobs": null, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + } + - title: Streaming + request: + curl: | + curl https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "VAR_model_id", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello!" + } + ], + "stream": true + }' + python: | + from openai import OpenAI + client = OpenAI() + + completion = client.chat.completions.create( + model="VAR_model_id", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ], + stream=True + ) + + for chunk in completion: + print(chunk.choices[0].delta) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const completion = await openai.chat.completions.create({ + model: "VAR_model_id", + messages: [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ], + stream: true, + }); + + for await (const chunk of completion) { + console.log(chunk.choices[0].delta.content); + } + } + + main(); + response: | + {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]} + + {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null}]} + + .... + + {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + - title: Functions + request: + curl: | + curl https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-4o", + "messages": [ + { + "role": "user", + "content": "What'\''s the weather like in Boston today?" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } + } + ], + "tool_choice": "auto" + }' + python: | + from openai import OpenAI + client = OpenAI() + + tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + }, + } + } + ] + messages = [{"role": "user", "content": "What's the weather like in Boston today?"}] + completion = client.chat.completions.create( + model="VAR_model_id", + messages=messages, + tools=tools, + tool_choice="auto" + ) + + print(completion) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const messages = [{"role": "user", "content": "What's the weather like in Boston today?"}]; + const tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + }, + } + } + ]; + + const response = await openai.chat.completions.create({ + model: "gpt-4o", + messages: messages, + tools: tools, + tool_choice: "auto", + }); + + console.log(response); + } + + main(); + response: | + { + "id": "chatcmpl-abc123", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_abc123", + "type": "function", + "function": { + "name": "get_current_weather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } + } + - title: Logprobs + request: + curl: | + curl https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "VAR_model_id", + "messages": [ + { + "role": "user", + "content": "Hello!" + } + ], + "logprobs": true, + "top_logprobs": 2 + }' + python: | + from openai import OpenAI + client = OpenAI() + + completion = client.chat.completions.create( + model="VAR_model_id", + messages=[ + {"role": "user", "content": "Hello!"} + ], + logprobs=True, + top_logprobs=2 + ) + + print(completion.choices[0].message) + print(completion.choices[0].logprobs) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const completion = await openai.chat.completions.create({ + messages: [{ role: "user", content: "Hello!" }], + model: "VAR_model_id", + logprobs: true, + top_logprobs: 2, + }); + + console.log(completion.choices[0]); + } + + main(); + response: | + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1702685778, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?" + }, + "logprobs": { + "content": [ + { + "token": "Hello", + "logprob": -0.31725305, + "bytes": [72, 101, 108, 108, 111], + "top_logprobs": [ + { + "token": "Hello", + "logprob": -0.31725305, + "bytes": [72, 101, 108, 108, 111] + }, + { + "token": "Hi", + "logprob": -1.3190403, + "bytes": [72, 105] + } + ] + }, + { + "token": "!", + "logprob": -0.02380986, + "bytes": [ + 33 + ], + "top_logprobs": [ + { + "token": "!", + "logprob": -0.02380986, + "bytes": [33] + }, + { + "token": " there", + "logprob": -3.787621, + "bytes": [32, 116, 104, 101, 114, 101] + } + ] + }, + { + "token": " How", + "logprob": -0.000054669687, + "bytes": [32, 72, 111, 119], + "top_logprobs": [ + { + "token": " How", + "logprob": -0.000054669687, + "bytes": [32, 72, 111, 119] + }, + { + "token": "<|end|>", + "logprob": -10.953937, + "bytes": null + } + ] + }, + { + "token": " can", + "logprob": -0.015801601, + "bytes": [32, 99, 97, 110], + "top_logprobs": [ + { + "token": " can", + "logprob": -0.015801601, + "bytes": [32, 99, 97, 110] + }, + { + "token": " may", + "logprob": -4.161023, + "bytes": [32, 109, 97, 121] + } + ] + }, + { + "token": " I", + "logprob": -3.7697225e-6, + "bytes": [ + 32, + 73 + ], + "top_logprobs": [ + { + "token": " I", + "logprob": -3.7697225e-6, + "bytes": [32, 73] + }, + { + "token": " assist", + "logprob": -13.596657, + "bytes": [32, 97, 115, 115, 105, 115, 116] + } + ] + }, + { + "token": " assist", + "logprob": -0.04571125, + "bytes": [32, 97, 115, 115, 105, 115, 116], + "top_logprobs": [ + { + "token": " assist", + "logprob": -0.04571125, + "bytes": [32, 97, 115, 115, 105, 115, 116] + }, + { + "token": " help", + "logprob": -3.1089056, + "bytes": [32, 104, 101, 108, 112] + } + ] + }, + { + "token": " you", + "logprob": -5.4385737e-6, + "bytes": [32, 121, 111, 117], + "top_logprobs": [ + { + "token": " you", + "logprob": -5.4385737e-6, + "bytes": [32, 121, 111, 117] + }, + { + "token": " today", + "logprob": -12.807695, + "bytes": [32, 116, 111, 100, 97, 121] + } + ] + }, + { + "token": " today", + "logprob": -0.0040071653, + "bytes": [32, 116, 111, 100, 97, 121], + "top_logprobs": [ + { + "token": " today", + "logprob": -0.0040071653, + "bytes": [32, 116, 111, 100, 97, 121] + }, + { + "token": "?", + "logprob": -5.5247097, + "bytes": [63] + } + ] + }, + { + "token": "?", + "logprob": -0.0008108172, + "bytes": [63], + "top_logprobs": [ + { + "token": "?", + "logprob": -0.0008108172, + "bytes": [63] + }, + { + "token": "?\n", + "logprob": -7.184561, + "bytes": [63, 10] + } + ] + } + ] + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 9, + "total_tokens": 18 + }, + "system_fingerprint": null + } + /completions: + post: + operationId: createCompletion + tags: + - Completions + - text-generation + summary: Creates a completion for the provided prompt and parameters. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCompletionRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCompletionResponse' + x-oaiMeta: + name: Create completion + group: completions + returns: | + Returns a [completion](/docs/api-reference/completions/object) object, or a sequence of completion objects if the request is streamed. + legacy: true + examples: + - title: No streaming + request: + curl: | + curl https://api.openai.com/v1/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "VAR_model_id", + "prompt": "Say this is a test", + "max_tokens": 7, + "temperature": 0 + }' + python: | + from openai import OpenAI + client = OpenAI() + + client.completions.create( + model="VAR_model_id", + prompt="Say this is a test", + max_tokens=7, + temperature=0 + ) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const completion = await openai.completions.create({ + model: "VAR_model_id", + prompt: "Say this is a test.", + max_tokens: 7, + temperature: 0, + }); + + console.log(completion); + } + main(); + response: | + { + "id": "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7", + "object": "text_completion", + "created": 1589478378, + "model": "VAR_model_id", + "system_fingerprint": "fp_44709d6fcb", + "choices": [ + { + "text": "\n\nThis is indeed a test", + "index": 0, + "logprobs": null, + "finish_reason": "length" + } + ], + "usage": { + "prompt_tokens": 5, + "completion_tokens": 7, + "total_tokens": 12 + } + } + - title: Streaming + request: + curl: | + curl https://api.openai.com/v1/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "VAR_model_id", + "prompt": "Say this is a test", + "max_tokens": 7, + "temperature": 0, + "stream": true + }' + python: | + from openai import OpenAI + client = OpenAI() + + for chunk in client.completions.create( + model="VAR_model_id", + prompt="Say this is a test", + max_tokens=7, + temperature=0, + stream=True + ): + print(chunk.choices[0].text) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const stream = await openai.completions.create({ + model: "VAR_model_id", + prompt: "Say this is a test.", + stream: true, + }); + + for await (const chunk of stream) { + console.log(chunk.choices[0].text) + } + } + main(); + response: | + { + "id": "cmpl-7iA7iJjj8V2zOkCGvWF2hAkDWBQZe", + "object": "text_completion", + "created": 1690759702, + "choices": [ + { + "text": "This", + "index": 0, + "logprobs": null, + "finish_reason": null + } + ], + "model": "gpt-3.5-turbo-instruct" + "system_fingerprint": "fp_44709d6fcb", + } + /images/generations: + post: + operationId: createImage + tags: + - Images + summary: Creates an image given a prompt. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateImageRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ImagesResponse' + x-oaiMeta: + name: Create image + group: images + returns: Returns a list of [image](/docs/api-reference/images/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/images/generations \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "dall-e-3", + "prompt": "A cute baby sea otter", + "n": 1, + "size": "1024x1024" + }' + python: | + from openai import OpenAI + client = OpenAI() + + client.images.generate( + model="dall-e-3", + prompt="A cute baby sea otter", + n=1, + size="1024x1024" + ) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const image = await openai.images.generate({ model: "dall-e-3", prompt: "A cute baby sea otter" }); + + console.log(image.data); + } + main(); + response: | + { + "created": 1589478378, + "data": [ + { + "url": "https://..." + }, + { + "url": "https://..." + } + ] + } + /images/edits: + post: + operationId: createImageEdit + tags: + - Images + summary: Creates an edited or extended image given an original image and a prompt. + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateImageEditRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ImagesResponse' + x-oaiMeta: + name: Create image edit + group: images + returns: Returns a list of [image](/docs/api-reference/images/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/images/edits \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -F image="@otter.png" \ + -F mask="@mask.png" \ + -F prompt="A cute baby sea otter wearing a beret" \ + -F n=2 \ + -F size="1024x1024" + python: | + from openai import OpenAI + client = OpenAI() + + client.images.edit( + image=open("otter.png", "rb"), + mask=open("mask.png", "rb"), + prompt="A cute baby sea otter wearing a beret", + n=2, + size="1024x1024" + ) + node.js: |- + import fs from "fs"; + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const image = await openai.images.edit({ + image: fs.createReadStream("otter.png"), + mask: fs.createReadStream("mask.png"), + prompt: "A cute baby sea otter wearing a beret", + }); + + console.log(image.data); + } + main(); + response: | + { + "created": 1589478378, + "data": [ + { + "url": "https://..." + }, + { + "url": "https://..." + } + ] + } + /images/variations: + post: + operationId: createImageVariation + tags: + - Images + summary: Creates a variation of a given image. + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateImageVariationRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ImagesResponse' + x-oaiMeta: + name: Create image variation + group: images + returns: Returns a list of [image](/docs/api-reference/images/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/images/variations \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -F image="@otter.png" \ + -F n=2 \ + -F size="1024x1024" + python: | + from openai import OpenAI + client = OpenAI() + + response = client.images.create_variation( + image=open("image_edit_original.png", "rb"), + n=2, + size="1024x1024" + ) + node.js: |- + import fs from "fs"; + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const image = await openai.images.createVariation({ + image: fs.createReadStream("otter.png"), + }); + + console.log(image.data); + } + main(); + response: | + { + "created": 1589478378, + "data": [ + { + "url": "https://..." + }, + { + "url": "https://..." + } + ] + } + /embeddings: + post: + operationId: createEmbedding + tags: + - Embeddings + summary: Creates an embedding vector representing the input text. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEmbeddingRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEmbeddingResponse' + x-oaiMeta: + name: Create embeddings + group: embeddings + returns: A list of [embedding](/docs/api-reference/embeddings/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/embeddings \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "input": "The food was delicious and the waiter...", + "model": "text-embedding-ada-002", + "encoding_format": "float" + }' + python: | + from openai import OpenAI + client = OpenAI() + + client.embeddings.create( + model="text-embedding-ada-002", + input="The food was delicious and the waiter...", + encoding_format="float" + ) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const embedding = await openai.embeddings.create({ + model: "text-embedding-ada-002", + input: "The quick brown fox jumped over the lazy dog", + encoding_format: "float", + }); + + console.log(embedding); + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "object": "embedding", + "embedding": [ + 0.0023064255, + -0.009327292, + .... (1536 floats total for ada-002) + -0.0028842222, + ], + "index": 0 + } + ], + "model": "text-embedding-ada-002", + "usage": { + "prompt_tokens": 8, + "total_tokens": 8 + } + } + /audio/speech: + post: + operationId: createSpeech + tags: + - Audio + summary: Generates audio from the input text. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSpeechRequest' + responses: + '200': + description: OK + headers: + Transfer-Encoding: + schema: + type: string + description: chunked + content: + application/octet-stream: + schema: + type: string + format: binary + x-oaiMeta: + name: Create speech + group: audio + returns: The audio file content. + examples: + request: + curl: | + curl https://api.openai.com/v1/audio/speech \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "tts-1", + "input": "The quick brown fox jumped over the lazy dog.", + "voice": "alloy" + }' \ + --output speech.mp3 + python: | + from pathlib import Path + import openai + + speech_file_path = Path(__file__).parent / "speech.mp3" + response = openai.audio.speech.create( + model="tts-1", + voice="alloy", + input="The quick brown fox jumped over the lazy dog." + ) + response.stream_to_file(speech_file_path) + node: | + import fs from "fs"; + import path from "path"; + import OpenAI from "openai"; + + const openai = new OpenAI(); + + const speechFile = path.resolve("./speech.mp3"); + + async function main() { + const mp3 = await openai.audio.speech.create({ + model: "tts-1", + voice: "alloy", + input: "Today is a wonderful day to build something people love!", + }); + console.log(speechFile); + const buffer = Buffer.from(await mp3.arrayBuffer()); + await fs.promises.writeFile(speechFile, buffer); + } + main(); + /audio/transcriptions: + post: + operationId: createTranscription + tags: + - Audio + summary: Transcribes audio into the input language. + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateTranscriptionRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/CreateTranscriptionResponseJson' + - $ref: '#/components/schemas/CreateTranscriptionResponseVerboseJson' + x-oaiMeta: + name: Create transcription + group: audio + returns: The [transcription object](/docs/api-reference/audio/json-object) or a [verbose transcription object](/docs/api-reference/audio/verbose-json-object). + examples: + - title: Default + request: + curl: | + curl https://api.openai.com/v1/audio/transcriptions \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: multipart/form-data" \ + -F file="@/path/to/file/audio.mp3" \ + -F model="whisper-1" + python: | + from openai import OpenAI + client = OpenAI() + + audio_file = open("speech.mp3", "rb") + transcript = client.audio.transcriptions.create( + model="whisper-1", + file=audio_file + ) + node: | + import fs from "fs"; + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const transcription = await openai.audio.transcriptions.create({ + file: fs.createReadStream("audio.mp3"), + model: "whisper-1", + }); + + console.log(transcription.text); + } + main(); + response: | + { + "text": "Imagine the wildest idea that you've ever had, and you're curious about how it might scale to something that's a 100, a 1,000 times bigger. This is a place where you can get to do that." + } + - title: Word timestamps + request: + curl: | + curl https://api.openai.com/v1/audio/transcriptions \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: multipart/form-data" \ + -F file="@/path/to/file/audio.mp3" \ + -F "timestamp_granularities[]=word" \ + -F model="whisper-1" \ + -F response_format="verbose_json" + python: | + from openai import OpenAI + client = OpenAI() + + audio_file = open("speech.mp3", "rb") + transcript = client.audio.transcriptions.create( + file=audio_file, + model="whisper-1", + response_format="verbose_json", + timestamp_granularities=["word"] + ) + + print(transcript.words) + node: | + import fs from "fs"; + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const transcription = await openai.audio.transcriptions.create({ + file: fs.createReadStream("audio.mp3"), + model: "whisper-1", + response_format: "verbose_json", + timestamp_granularities: ["word"] + }); + + console.log(transcription.text); + } + main(); + response: | + { + "task": "transcribe", + "language": "english", + "duration": 8.470000267028809, + "text": "The beach was a popular spot on a hot summer day. People were swimming in the ocean, building sandcastles, and playing beach volleyball.", + "words": [ + { + "word": "The", + "start": 0.0, + "end": 0.23999999463558197 + }, + ... + { + "word": "volleyball", + "start": 7.400000095367432, + "end": 7.900000095367432 + } + ] + } + - title: Segment timestamps + request: + curl: | + curl https://api.openai.com/v1/audio/transcriptions \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: multipart/form-data" \ + -F file="@/path/to/file/audio.mp3" \ + -F "timestamp_granularities[]=segment" \ + -F model="whisper-1" \ + -F response_format="verbose_json" + python: | + from openai import OpenAI + client = OpenAI() + + audio_file = open("speech.mp3", "rb") + transcript = client.audio.transcriptions.create( + file=audio_file, + model="whisper-1", + response_format="verbose_json", + timestamp_granularities=["segment"] + ) + + print(transcript.words) + node: | + import fs from "fs"; + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const transcription = await openai.audio.transcriptions.create({ + file: fs.createReadStream("audio.mp3"), + model: "whisper-1", + response_format: "verbose_json", + timestamp_granularities: ["segment"] + }); + + console.log(transcription.text); + } + main(); + response: | + { + "task": "transcribe", + "language": "english", + "duration": 8.470000267028809, + "text": "The beach was a popular spot on a hot summer day. People were swimming in the ocean, building sandcastles, and playing beach volleyball.", + "segments": [ + { + "id": 0, + "seek": 0, + "start": 0.0, + "end": 3.319999933242798, + "text": " The beach was a popular spot on a hot summer day.", + "tokens": [ + 50364, 440, 7534, 390, 257, 3743, 4008, 322, 257, 2368, 4266, 786, 13, 50530 + ], + "temperature": 0.0, + "avg_logprob": -0.2860786020755768, + "compression_ratio": 1.2363636493682861, + "no_speech_prob": 0.00985979475080967 + }, + ... + ] + } + /audio/translations: + post: + operationId: createTranslation + tags: + - Audio + summary: Translates audio into English. + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateTranslationRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/CreateTranslationResponseJson' + - $ref: '#/components/schemas/CreateTranslationResponseVerboseJson' + x-oaiMeta: + name: Create translation + group: audio + returns: The translated text. + examples: + request: + curl: | + curl https://api.openai.com/v1/audio/translations \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: multipart/form-data" \ + -F file="@/path/to/file/german.m4a" \ + -F model="whisper-1" + python: | + from openai import OpenAI + client = OpenAI() + + audio_file = open("speech.mp3", "rb") + transcript = client.audio.translations.create( + model="whisper-1", + file=audio_file + ) + node: | + import fs from "fs"; + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const translation = await openai.audio.translations.create({ + file: fs.createReadStream("speech.mp3"), + model: "whisper-1", + }); + + console.log(translation.text); + } + main(); + response: | + { + "text": "Hello, my name is Wolfgang and I come from Germany. Where are you heading today?" + } + /files: + get: + operationId: listFiles + tags: + - Files + summary: Returns a list of files that belong to the user's organization. + parameters: + - in: query + name: purpose + required: false + schema: + type: string + description: Only return files with the given purpose. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListFilesResponse' + x-oaiMeta: + name: List files + group: files + returns: A list of [File](/docs/api-reference/files/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/files \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.files.list() + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const list = await openai.files.list(); + + for await (const file of list) { + console.log(file); + } + } + + main(); + response: | + { + "data": [ + { + "id": "file-abc123", + "object": "file", + "bytes": 175, + "created_at": 1613677385, + "filename": "salesOverview.pdf", + "purpose": "assistants", + }, + { + "id": "file-abc123", + "object": "file", + "bytes": 140, + "created_at": 1613779121, + "filename": "puppy.jsonl", + "purpose": "fine-tune", + } + ], + "object": "list" + } + post: + operationId: createFile + tags: + - Files + summary: | + Upload a file that can be used across various endpoints. Individual files can be up to 512 MB, and the size of all files uploaded by one organization can be up to 100 GB. + + The Assistants API supports files up to 2 million tokens and of specific file types. See the [Assistants Tools guide](/docs/assistants/tools) for details. + + The Fine-tuning API only supports `.jsonl` files. The input also has certain required formats for fine-tuning [chat](/docs/api-reference/fine-tuning/chat-input) or [completions](/docs/api-reference/fine-tuning/completions-input) models. + + The Batch API only supports `.jsonl` files up to 100 MB in size. The input also has a specific required [format](/docs/api-reference/batch/request-input). + + Please [contact us](https://help.openai.com/) if you need to increase these storage limits. + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateFileRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/OpenAIFile' + x-oaiMeta: + name: Upload file + group: files + returns: The uploaded [File](/docs/api-reference/files/object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/files \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -F purpose="fine-tune" \ + -F file="@mydata.jsonl" + python: | + from openai import OpenAI + client = OpenAI() + + client.files.create( + file=open("mydata.jsonl", "rb"), + purpose="fine-tune" + ) + node.js: |- + import fs from "fs"; + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const file = await openai.files.create({ + file: fs.createReadStream("mydata.jsonl"), + purpose: "fine-tune", + }); + + console.log(file); + } + + main(); + response: | + { + "id": "file-abc123", + "object": "file", + "bytes": 120000, + "created_at": 1677610602, + "filename": "mydata.jsonl", + "purpose": "fine-tune", + } + /files/{file_id}: + delete: + operationId: deleteFile + tags: + - Files + summary: Delete a file. + parameters: + - in: path + name: file_id + required: true + schema: + type: string + description: The ID of the file to use for this request. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteFileResponse' + x-oaiMeta: + name: Delete file + group: files + returns: Deletion status. + examples: + request: + curl: | + curl https://api.openai.com/v1/files/file-abc123 \ + -X DELETE \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.files.delete("file-abc123") + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const file = await openai.files.del("file-abc123"); + + console.log(file); + } + + main(); + response: | + { + "id": "file-abc123", + "object": "file", + "deleted": true + } + get: + operationId: retrieveFile + tags: + - Files + summary: Returns information about a specific file. + parameters: + - in: path + name: file_id + required: true + schema: + type: string + description: The ID of the file to use for this request. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/OpenAIFile' + x-oaiMeta: + name: Retrieve file + group: files + returns: The [File](/docs/api-reference/files/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/files/file-abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.files.retrieve("file-abc123") + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const file = await openai.files.retrieve("file-abc123"); + + console.log(file); + } + + main(); + response: | + { + "id": "file-abc123", + "object": "file", + "bytes": 120000, + "created_at": 1677610602, + "filename": "mydata.jsonl", + "purpose": "fine-tune", + } + /files/{file_id}/content: + get: + operationId: downloadFile + tags: + - Files + summary: Returns the contents of the specified file. + parameters: + - in: path + name: file_id + required: true + schema: + type: string + description: The ID of the file to use for this request. + responses: + '200': + description: OK + content: + application/json: + schema: + type: string + x-oaiMeta: + name: Retrieve file content + group: files + returns: The file content. + examples: + request: + curl: | + curl https://api.openai.com/v1/files/file-abc123/content \ + -H "Authorization: Bearer $OPENAI_API_KEY" > file.jsonl + python: | + from openai import OpenAI + client = OpenAI() + + content = client.files.content("file-abc123") + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const file = await openai.files.content("file-abc123"); + + console.log(file); + } + + main(); + /uploads: + post: + operationId: createUpload + tags: + - Uploads + summary: | + Creates an intermediate [Upload](/docs/api-reference/uploads/object) object that you can add [Parts](/docs/api-reference/uploads/part-object) to. Currently, an Upload can accept at most 8 GB in total and expires after an hour after you create it. + + Once you complete the Upload, we will create a [File](/docs/api-reference/files/object) object that contains all the parts you uploaded. This File is usable in the rest of our platform as a regular File object. + + For certain `purpose`s, the correct `mime_type` must be specified. Please refer to documentation for the supported MIME types for your use case: + - [Assistants](/docs/assistants/tools/file-search/supported-files) + + For guidance on the proper filename extensions for each purpose, please follow the documentation on [creating a File](/docs/api-reference/files/create). + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUploadRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Upload' + x-oaiMeta: + name: Create upload + group: uploads + returns: The [Upload](/docs/api-reference/uploads/object) object with status `pending`. + examples: + request: + curl: | + curl https://api.openai.com/v1/uploads \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "purpose": "fine-tune", + "filename": "training_examples.jsonl", + "bytes": 2147483648, + "mime_type": "text/jsonl" + }' + response: | + { + "id": "upload_abc123", + "object": "upload", + "bytes": 2147483648, + "created_at": 1719184911, + "filename": "training_examples.jsonl", + "purpose": "fine-tune", + "status": "pending", + "expires_at": 1719127296 + } + /uploads/{upload_id}/parts: + post: + operationId: addUploadPart + tags: + - Uploads + summary: | + Adds a [Part](/docs/api-reference/uploads/part-object) to an [Upload](/docs/api-reference/uploads/object) object. A Part represents a chunk of bytes from the file you are trying to upload. + + Each Part can be at most 64 MB, and you can add Parts until you hit the Upload maximum of 8 GB. + + It is possible to add multiple Parts in parallel. You can decide the intended order of the Parts when you [complete the Upload](/docs/api-reference/uploads/complete). + parameters: + - in: path + name: upload_id + required: true + schema: + type: string + example: upload_abc123 + description: | + The ID of the Upload. + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/AddUploadPartRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UploadPart' + x-oaiMeta: + name: Add upload part + group: uploads + returns: The upload [Part](/docs/api-reference/uploads/part-object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/uploads/upload_abc123/parts + -F data="aHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MS91cGxvYWRz..." + response: | + { + "id": "part_def456", + "object": "upload.part", + "created_at": 1719185911, + "upload_id": "upload_abc123" + } + /uploads/{upload_id}/complete: + post: + operationId: completeUpload + tags: + - Uploads + summary: | + Completes the [Upload](/docs/api-reference/uploads/object). + + Within the returned Upload object, there is a nested [File](/docs/api-reference/files/object) object that is ready to use in the rest of the platform. + + You can specify the order of the Parts by passing in an ordered list of the Part IDs. + + The number of bytes uploaded upon completion must match the number of bytes initially specified when creating the Upload object. No Parts may be added after an Upload is completed. + parameters: + - in: path + name: upload_id + required: true + schema: + type: string + example: upload_abc123 + description: | + The ID of the Upload. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CompleteUploadRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Upload' + x-oaiMeta: + name: Complete upload + group: uploads + returns: The [Upload](/docs/api-reference/uploads/object) object with status `completed` with an additional `file` property containing the created usable File object. + examples: + request: + curl: | + curl https://api.openai.com/v1/uploads/upload_abc123/complete + -d '{ + "part_ids": ["part_def456", "part_ghi789"] + }' + response: | + { + "id": "upload_abc123", + "object": "upload", + "bytes": 2147483648, + "created_at": 1719184911, + "filename": "training_examples.jsonl", + "purpose": "fine-tune", + "status": "completed", + "expires_at": 1719127296, + "file": { + "id": "file-xyz321", + "object": "file", + "bytes": 2147483648, + "created_at": 1719186911, + "filename": "training_examples.jsonl", + "purpose": "fine-tune", + } + } + /uploads/{upload_id}/cancel: + post: + operationId: cancelUpload + tags: + - Uploads + summary: | + Cancels the Upload. No Parts may be added after an Upload is cancelled. + parameters: + - in: path + name: upload_id + required: true + schema: + type: string + example: upload_abc123 + description: | + The ID of the Upload. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Upload' + x-oaiMeta: + name: Cancel upload + group: uploads + returns: The [Upload](/docs/api-reference/uploads/object) object with status `cancelled`. + examples: + request: + curl: | + curl https://api.openai.com/v1/uploads/upload_abc123/cancel + response: | + { + "id": "upload_abc123", + "object": "upload", + "bytes": 2147483648, + "created_at": 1719184911, + "filename": "training_examples.jsonl", + "purpose": "fine-tune", + "status": "cancelled", + "expires_at": 1719127296 + } + /fine_tuning/jobs: + post: + operationId: createFineTuningJob + tags: + - Fine-tuning + summary: | + Creates a fine-tuning job which begins the process of creating a new model from a given dataset. + + Response includes details of the enqueued job including job status and the name of the fine-tuned models once complete. + + [Learn more about fine-tuning](/docs/guides/fine-tuning) + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateFineTuningJobRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/FineTuningJob' + x-oaiMeta: + name: Create fine-tuning job + group: fine-tuning + returns: A [fine-tuning.job](/docs/api-reference/fine-tuning/object) object. + examples: + - title: Default + request: + curl: | + curl https://api.openai.com/v1/fine_tuning/jobs \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "training_file": "file-BK7bzQj3FfZFXr7DbL6xJwfo", + "model": "gpt-4o-mini" + }' + python: | + from openai import OpenAI + client = OpenAI() + + client.fine_tuning.jobs.create( + training_file="file-abc123", + model="gpt-4o-mini" + ) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const fineTune = await openai.fineTuning.jobs.create({ + training_file: "file-abc123" + }); + + console.log(fineTune); + } + + main(); + response: | + { + "object": "fine_tuning.job", + "id": "ftjob-abc123", + "model": "gpt-4o-mini-2024-07-18", + "created_at": 1721764800, + "fine_tuned_model": null, + "organization_id": "org-123", + "result_files": [], + "status": "queued", + "validation_file": null, + "training_file": "file-abc123", + } + - title: Epochs + request: + curl: | + curl https://api.openai.com/v1/fine_tuning/jobs \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "training_file": "file-abc123", + "model": "gpt-4o-mini", + "hyperparameters": { + "n_epochs": 2 + } + }' + python: | + from openai import OpenAI + client = OpenAI() + + client.fine_tuning.jobs.create( + training_file="file-abc123", + model="gpt-4o-mini", + hyperparameters={ + "n_epochs":2 + } + ) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const fineTune = await openai.fineTuning.jobs.create({ + training_file: "file-abc123", + model: "gpt-4o-mini", + hyperparameters: { n_epochs: 2 } + }); + + console.log(fineTune); + } + + main(); + response: | + { + "object": "fine_tuning.job", + "id": "ftjob-abc123", + "model": "gpt-4o-mini-2024-07-18", + "created_at": 1721764800, + "fine_tuned_model": null, + "organization_id": "org-123", + "result_files": [], + "status": "queued", + "validation_file": null, + "training_file": "file-abc123", + "hyperparameters": {"n_epochs": 2}, + } + - title: Validation file + request: + curl: | + curl https://api.openai.com/v1/fine_tuning/jobs \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "training_file": "file-abc123", + "validation_file": "file-abc123", + "model": "gpt-4o-mini" + }' + python: | + from openai import OpenAI + client = OpenAI() + + client.fine_tuning.jobs.create( + training_file="file-abc123", + validation_file="file-def456", + model="gpt-4o-mini" + ) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const fineTune = await openai.fineTuning.jobs.create({ + training_file: "file-abc123", + validation_file: "file-abc123" + }); + + console.log(fineTune); + } + + main(); + response: | + { + "object": "fine_tuning.job", + "id": "ftjob-abc123", + "model": "gpt-4o-mini-2024-07-18", + "created_at": 1721764800, + "fine_tuned_model": null, + "organization_id": "org-123", + "result_files": [], + "status": "queued", + "validation_file": "file-abc123", + "training_file": "file-abc123", + } + - title: W&B Integration + request: + curl: | + curl https://api.openai.com/v1/fine_tuning/jobs \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "training_file": "file-abc123", + "validation_file": "file-abc123", + "model": "gpt-4o-mini", + "integrations": [ + { + "type": "wandb", + "wandb": { + "project": "my-wandb-project", + "name": "ft-run-display-name" + "tags": [ + "first-experiment", "v2" + ] + } + } + ] + }' + response: | + { + "object": "fine_tuning.job", + "id": "ftjob-abc123", + "model": "gpt-4o-mini-2024-07-18", + "created_at": 1721764800, + "fine_tuned_model": null, + "organization_id": "org-123", + "result_files": [], + "status": "queued", + "validation_file": "file-abc123", + "training_file": "file-abc123", + "integrations": [ + { + "type": "wandb", + "wandb": { + "project": "my-wandb-project", + "entity": None, + "run_id": "ftjob-abc123" + } + } + ] + } + get: + operationId: listPaginatedFineTuningJobs + tags: + - Fine-tuning + summary: | + List your organization's fine-tuning jobs + parameters: + - name: after + in: query + description: Identifier for the last job from the previous pagination request. + required: false + schema: + type: string + - name: limit + in: query + description: Number of fine-tuning jobs to retrieve. + required: false + schema: + type: integer + default: 20 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListPaginatedFineTuningJobsResponse' + x-oaiMeta: + name: List fine-tuning jobs + group: fine-tuning + returns: A list of paginated [fine-tuning job](/docs/api-reference/fine-tuning/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/fine_tuning/jobs?limit=2 \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.fine_tuning.jobs.list() + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const list = await openai.fineTuning.jobs.list(); + + for await (const fineTune of list) { + console.log(fineTune); + } + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "object": "fine_tuning.job.event", + "id": "ft-event-TjX0lMfOniCZX64t9PUQT5hn", + "created_at": 1689813489, + "level": "warn", + "message": "Fine tuning process stopping due to job cancellation", + "data": null, + "type": "message" + }, + { ... }, + { ... } + ], "has_more": true + } + /fine_tuning/jobs/{fine_tuning_job_id}: + get: + operationId: retrieveFineTuningJob + tags: + - Fine-tuning + summary: | + Get info about a fine-tuning job. + + [Learn more about fine-tuning](/docs/guides/fine-tuning) + parameters: + - in: path + name: fine_tuning_job_id + required: true + schema: + type: string + example: ft-AF1WoRqd3aJAHsqc9NY7iL8F + description: | + The ID of the fine-tuning job. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/FineTuningJob' + x-oaiMeta: + name: Retrieve fine-tuning job + group: fine-tuning + returns: The [fine-tuning](/docs/api-reference/fine-tuning/object) object with the given ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/fine_tuning/jobs/ft-AF1WoRqd3aJAHsqc9NY7iL8F \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.fine_tuning.jobs.retrieve("ftjob-abc123") + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const fineTune = await openai.fineTuning.jobs.retrieve("ftjob-abc123"); + + console.log(fineTune); + } + + main(); + response: | + { + "object": "fine_tuning.job", + "id": "ftjob-abc123", + "model": "davinci-002", + "created_at": 1692661014, + "finished_at": 1692661190, + "fine_tuned_model": "ft:davinci-002:my-org:custom_suffix:7q8mpxmy", + "organization_id": "org-123", + "result_files": [ + "file-abc123" + ], + "status": "succeeded", + "validation_file": null, + "training_file": "file-abc123", + "hyperparameters": { + "n_epochs": 4, + "batch_size": 1, + "learning_rate_multiplier": 1.0 + }, + "trained_tokens": 5768, + "integrations": [], + "seed": 0, + "estimated_finish": 0 + } + /fine_tuning/jobs/{fine_tuning_job_id}/events: + get: + operationId: listFineTuningEvents + tags: + - Fine-tuning + summary: | + Get status updates for a fine-tuning job. + parameters: + - in: path + name: fine_tuning_job_id + required: true + schema: + type: string + example: ft-AF1WoRqd3aJAHsqc9NY7iL8F + description: | + The ID of the fine-tuning job to get events for. + - name: after + in: query + description: Identifier for the last event from the previous pagination request. + required: false + schema: + type: string + - name: limit + in: query + description: Number of events to retrieve. + required: false + schema: + type: integer + default: 20 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListFineTuningJobEventsResponse' + x-oaiMeta: + name: List fine-tuning events + group: fine-tuning + returns: A list of fine-tuning event objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/fine_tuning/jobs/ftjob-abc123/events \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.fine_tuning.jobs.list_events( + fine_tuning_job_id="ftjob-abc123", + limit=2 + ) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const list = await openai.fineTuning.list_events(id="ftjob-abc123", limit=2); + + for await (const fineTune of list) { + console.log(fineTune); + } + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "object": "fine_tuning.job.event", + "id": "ft-event-ddTJfwuMVpfLXseO0Am0Gqjm", + "created_at": 1721764800, + "level": "info", + "message": "Fine tuning job successfully completed", + "data": null, + "type": "message" + }, + { + "object": "fine_tuning.job.event", + "id": "ft-event-tyiGuB72evQncpH87xe505Sv", + "created_at": 1721764800, + "level": "info", + "message": "New fine-tuned model created: ft:gpt-4o-mini:openai::7p4lURel", + "data": null, + "type": "message" + } + ], + "has_more": true + } + /fine_tuning/jobs/{fine_tuning_job_id}/cancel: + post: + operationId: cancelFineTuningJob + tags: + - Fine-tuning + summary: | + Immediately cancel a fine-tune job. + parameters: + - in: path + name: fine_tuning_job_id + required: true + schema: + type: string + example: ft-AF1WoRqd3aJAHsqc9NY7iL8F + description: | + The ID of the fine-tuning job to cancel. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/FineTuningJob' + x-oaiMeta: + name: Cancel fine-tuning + group: fine-tuning + returns: The cancelled [fine-tuning](/docs/api-reference/fine-tuning/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/fine_tuning/jobs/ftjob-abc123/cancel \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.fine_tuning.jobs.cancel("ftjob-abc123") + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const fineTune = await openai.fineTuning.jobs.cancel("ftjob-abc123"); + + console.log(fineTune); + } + main(); + response: | + { + "object": "fine_tuning.job", + "id": "ftjob-abc123", + "model": "gpt-4o-mini-2024-07-18", + "created_at": 1721764800, + "fine_tuned_model": null, + "organization_id": "org-123", + "result_files": [], + "hyperparameters": { + "n_epochs": "auto" + }, + "status": "cancelled", + "validation_file": "file-abc123", + "training_file": "file-abc123" + } + /fine_tuning/jobs/{fine_tuning_job_id}/checkpoints: + get: + operationId: listFineTuningJobCheckpoints + tags: + - Fine-tuning + summary: | + List checkpoints for a fine-tuning job. + parameters: + - in: path + name: fine_tuning_job_id + required: true + schema: + type: string + example: ft-AF1WoRqd3aJAHsqc9NY7iL8F + description: | + The ID of the fine-tuning job to get checkpoints for. + - name: after + in: query + description: Identifier for the last checkpoint ID from the previous pagination request. + required: false + schema: + type: string + - name: limit + in: query + description: Number of checkpoints to retrieve. + required: false + schema: + type: integer + default: 10 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListFineTuningJobCheckpointsResponse' + x-oaiMeta: + name: List fine-tuning checkpoints + group: fine-tuning + returns: A list of fine-tuning [checkpoint objects](/docs/api-reference/fine-tuning/checkpoint-object) for a fine-tuning job. + examples: + request: + curl: | + curl https://api.openai.com/v1/fine_tuning/jobs/ftjob-abc123/checkpoints \ + -H "Authorization: Bearer $OPENAI_API_KEY" + response: | + { + "object": "list" + "data": [ + { + "object": "fine_tuning.job.checkpoint", + "id": "ftckpt_zc4Q7MP6XxulcVzj4MZdwsAB", + "created_at": 1721764867, + "fine_tuned_model_checkpoint": "ft:gpt-4o-mini-2024-07-18:my-org:custom-suffix:96olL566:ckpt-step-2000", + "metrics": { + "full_valid_loss": 0.134, + "full_valid_mean_token_accuracy": 0.874 + }, + "fine_tuning_job_id": "ftjob-abc123", + "step_number": 2000, + }, + { + "object": "fine_tuning.job.checkpoint", + "id": "ftckpt_enQCFmOTGj3syEpYVhBRLTSy", + "created_at": 1721764800, + "fine_tuned_model_checkpoint": "ft:gpt-4o-mini-2024-07-18:my-org:custom-suffix:7q8mpxmy:ckpt-step-1000", + "metrics": { + "full_valid_loss": 0.167, + "full_valid_mean_token_accuracy": 0.781 + }, + "fine_tuning_job_id": "ftjob-abc123", + "step_number": 1000, + }, + ], + "first_id": "ftckpt_zc4Q7MP6XxulcVzj4MZdwsAB", + "last_id": "ftckpt_enQCFmOTGj3syEpYVhBRLTSy", + "has_more": true + } + /models: + get: + operationId: listModels + tags: + - Models + summary: Lists the currently available models, and provides basic information about each one such as the owner and availability. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListModelsResponse' + x-oaiMeta: + name: List models + group: models + returns: A list of [model](/docs/api-reference/models/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/models \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.models.list() + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const list = await openai.models.list(); + + for await (const model of list) { + console.log(model); + } + } + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "model-id-0", + "object": "model", + "created": 1686935002, + "owned_by": "organization-owner" + }, + { + "id": "model-id-1", + "object": "model", + "created": 1686935002, + "owned_by": "organization-owner", + }, + { + "id": "model-id-2", + "object": "model", + "created": 1686935002, + "owned_by": "openai" + }, + ], + "object": "list" + } + /models/{model}: + get: + operationId: retrieveModel + tags: + - Models + summary: Retrieves a model instance, providing basic information about the model such as the owner and permissioning. + parameters: + - in: path + name: model + required: true + schema: + type: string + example: gpt-4o-mini + description: The ID of the model to use for this request + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Model' + x-oaiMeta: + name: Retrieve model + group: models + returns: The [model](/docs/api-reference/models/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/models/VAR_model_id \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.models.retrieve("VAR_model_id") + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const model = await openai.models.retrieve("VAR_model_id"); + + console.log(model); + } + + main(); + response: | + { + "id": "VAR_model_id", + "object": "model", + "created": 1686935002, + "owned_by": "openai" + } + delete: + operationId: deleteModel + tags: + - Models + summary: Delete a fine-tuned model. You must have the Owner role in your organization to delete a model. + parameters: + - in: path + name: model + required: true + schema: + type: string + example: ft:gpt-4o-mini:acemeco:suffix:abc123 + description: The model to delete + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteModelResponse' + x-oaiMeta: + name: Delete a fine-tuned model + group: models + returns: Deletion status. + examples: + request: + curl: | + curl https://api.openai.com/v1/models/ft:gpt-4o-mini:acemeco:suffix:abc123 \ + -X DELETE \ + -H "Authorization: Bearer $OPENAI_API_KEY" + python: | + from openai import OpenAI + client = OpenAI() + + client.models.delete("ft:gpt-4o-mini:acemeco:suffix:abc123") + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const model = await openai.models.del("ft:gpt-4o-mini:acemeco:suffix:abc123"); + + console.log(model); + } + main(); + response: | + { + "id": "ft:gpt-4o-mini:acemeco:suffix:abc123", + "object": "model", + "deleted": true + } + /moderations: + post: + operationId: createModeration + tags: + - Moderations + summary: Classifies if text is potentially harmful. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateModerationRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateModerationResponse' + x-oaiMeta: + name: Create moderation + group: moderations + returns: A [moderation](/docs/api-reference/moderations/object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/moderations \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "input": "I want to kill them." + }' + python: | + from openai import OpenAI + client = OpenAI() + + moderation = client.moderations.create(input="I want to kill them.") + print(moderation) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const moderation = await openai.moderations.create({ input: "I want to kill them." }); + + console.log(moderation); + } + main(); + response: | + { + "id": "modr-XXXXX", + "model": "text-moderation-005", + "results": [ + { + "flagged": true, + "categories": { + "sexual": false, + "hate": false, + "harassment": false, + "self-harm": false, + "sexual/minors": false, + "hate/threatening": false, + "violence/graphic": false, + "self-harm/intent": false, + "self-harm/instructions": false, + "harassment/threatening": true, + "violence": true, + }, + "category_scores": { + "sexual": 1.2282071e-06, + "hate": 0.010696256, + "harassment": 0.29842457, + "self-harm": 1.5236925e-08, + "sexual/minors": 5.7246268e-08, + "hate/threatening": 0.0060676364, + "violence/graphic": 4.435014e-06, + "self-harm/intent": 8.098441e-10, + "self-harm/instructions": 2.8498655e-11, + "harassment/threatening": 0.63055265, + "violence": 0.99011886, + } + } + ] + } + /assistants: + get: + operationId: listAssistants + tags: + - Assistants + summary: Returns a list of assistants. + parameters: + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: order + in: query + description: | + Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + schema: + type: string + default: desc + enum: + - asc + - desc + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + schema: + type: string + - name: before + in: query + description: | + A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListAssistantsResponse' + x-oaiMeta: + name: List assistants + group: assistants + beta: true + returns: A list of [assistant](/docs/api-reference/assistants/object) objects. + examples: + request: + curl: | + curl "https://api.openai.com/v1/assistants?order=desc&limit=20" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + my_assistants = client.beta.assistants.list( + order="desc", + limit="20", + ) + print(my_assistants.data) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const myAssistants = await openai.beta.assistants.list({ + order: "desc", + limit: "20", + }); + + console.log(myAssistants.data); + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698982736, + "name": "Coding Tutor", + "description": null, + "model": "gpt-4o", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "tool_resources": {}, + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + }, + { + "id": "asst_abc456", + "object": "assistant", + "created_at": 1698982718, + "name": "My Assistant", + "description": null, + "model": "gpt-4o", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "tool_resources": {}, + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + }, + { + "id": "asst_abc789", + "object": "assistant", + "created_at": 1698982643, + "name": null, + "description": null, + "model": "gpt-4o", + "instructions": null, + "tools": [], + "tool_resources": {}, + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + } + ], + "first_id": "asst_abc123", + "last_id": "asst_abc789", + "has_more": false + } + post: + operationId: createAssistant + tags: + - Assistants + summary: Create an assistant with a model and instructions. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAssistantRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AssistantObject' + x-oaiMeta: + name: Create assistant + group: assistants + beta: true + returns: An [assistant](/docs/api-reference/assistants/object) object. + examples: + - title: Code Interpreter + request: + curl: | + curl "https://api.openai.com/v1/assistants" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + "name": "Math Tutor", + "tools": [{"type": "code_interpreter"}], + "model": "gpt-4o" + }' + python: | + from openai import OpenAI + client = OpenAI() + + my_assistant = client.beta.assistants.create( + instructions="You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + name="Math Tutor", + tools=[{"type": "code_interpreter"}], + model="gpt-4o", + ) + print(my_assistant) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const myAssistant = await openai.beta.assistants.create({ + instructions: + "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + name: "Math Tutor", + tools: [{ type: "code_interpreter" }], + model: "gpt-4o", + }); + + console.log(myAssistant); + } + + main(); + response: | + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": "Math Tutor", + "description": null, + "model": "gpt-4o", + "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + "tools": [ + { + "type": "code_interpreter" + } + ], + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + } + - title: Files + request: + curl: | + curl https://api.openai.com/v1/assistants \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "instructions": "You are an HR bot, and you have access to files to answer employee questions about company policies.", + "tools": [{"type": "file_search"}], + "tool_resources": {"file_search": {"vector_store_ids": ["vs_123"]}}, + "model": "gpt-4o" + }' + python: | + from openai import OpenAI + client = OpenAI() + + my_assistant = client.beta.assistants.create( + instructions="You are an HR bot, and you have access to files to answer employee questions about company policies.", + name="HR Helper", + tools=[{"type": "file_search"}], + tool_resources={"file_search": {"vector_store_ids": ["vs_123"]}}, + model="gpt-4o" + ) + print(my_assistant) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const myAssistant = await openai.beta.assistants.create({ + instructions: + "You are an HR bot, and you have access to files to answer employee questions about company policies.", + name: "HR Helper", + tools: [{ type: "file_search" }], + tool_resources: { + file_search: { + vector_store_ids: ["vs_123"] + } + }, + model: "gpt-4o" + }); + + console.log(myAssistant); + } + + main(); + response: | + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1699009403, + "name": "HR Helper", + "description": null, + "model": "gpt-4o", + "instructions": "You are an HR bot, and you have access to files to answer employee questions about company policies.", + "tools": [ + { + "type": "file_search" + } + ], + "tool_resources": { + "file_search": { + "vector_store_ids": ["vs_123"] + } + }, + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + } + /assistants/{assistant_id}: + get: + operationId: getAssistant + tags: + - Assistants + summary: Retrieves an assistant. + parameters: + - in: path + name: assistant_id + required: true + schema: + type: string + description: The ID of the assistant to retrieve. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AssistantObject' + x-oaiMeta: + name: Retrieve assistant + group: assistants + beta: true + returns: The [assistant](/docs/api-reference/assistants/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/assistants/asst_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + my_assistant = client.beta.assistants.retrieve("asst_abc123") + print(my_assistant) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const myAssistant = await openai.beta.assistants.retrieve( + "asst_abc123" + ); + + console.log(myAssistant); + } + + main(); + response: | + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1699009709, + "name": "HR Helper", + "description": null, + "model": "gpt-4o", + "instructions": "You are an HR bot, and you have access to files to answer employee questions about company policies.", + "tools": [ + { + "type": "file_search" + } + ], + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + } + post: + operationId: modifyAssistant + tags: + - Assistants + summary: Modifies an assistant. + parameters: + - in: path + name: assistant_id + required: true + schema: + type: string + description: The ID of the assistant to modify. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyAssistantRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AssistantObject' + x-oaiMeta: + name: Modify assistant + group: assistants + beta: true + returns: The modified [assistant](/docs/api-reference/assistants/object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/assistants/asst_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "instructions": "You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.", + "tools": [{"type": "file_search"}], + "model": "gpt-4o" + }' + python: | + from openai import OpenAI + client = OpenAI() + + my_updated_assistant = client.beta.assistants.update( + "asst_abc123", + instructions="You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.", + name="HR Helper", + tools=[{"type": "file_search"}], + model="gpt-4o" + ) + + print(my_updated_assistant) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const myUpdatedAssistant = await openai.beta.assistants.update( + "asst_abc123", + { + instructions: + "You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.", + name: "HR Helper", + tools: [{ type: "file_search" }], + model: "gpt-4o" + } + ); + + console.log(myUpdatedAssistant); + } + + main(); + response: | + { + "id": "asst_123", + "object": "assistant", + "created_at": 1699009709, + "name": "HR Helper", + "description": null, + "model": "gpt-4o", + "instructions": "You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.", + "tools": [ + { + "type": "file_search" + } + ], + "tool_resources": { + "file_search": { + "vector_store_ids": [] + } + }, + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + } + delete: + operationId: deleteAssistant + tags: + - Assistants + summary: Delete an assistant. + parameters: + - in: path + name: assistant_id + required: true + schema: + type: string + description: The ID of the assistant to delete. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteAssistantResponse' + x-oaiMeta: + name: Delete assistant + group: assistants + beta: true + returns: Deletion status + examples: + request: + curl: | + curl https://api.openai.com/v1/assistants/asst_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -X DELETE + python: | + from openai import OpenAI + client = OpenAI() + + response = client.beta.assistants.delete("asst_abc123") + print(response) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const response = await openai.beta.assistants.del("asst_abc123"); + + console.log(response); + } + main(); + response: | + { + "id": "asst_abc123", + "object": "assistant.deleted", + "deleted": true + } + /threads: + post: + operationId: createThread + tags: + - Assistants + summary: Create a thread. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateThreadRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ThreadObject' + x-oaiMeta: + name: Create thread + group: threads + beta: true + returns: A [thread](/docs/api-reference/threads) object. + examples: + - title: Empty + request: + curl: | + curl https://api.openai.com/v1/threads \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '' + python: | + from openai import OpenAI + client = OpenAI() + + empty_thread = client.beta.threads.create() + print(empty_thread) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const emptyThread = await openai.beta.threads.create(); + + console.log(emptyThread); + } + + main(); + response: | + { + "id": "thread_abc123", + "object": "thread", + "created_at": 1699012949, + "metadata": {}, + "tool_resources": {} + } + - title: Messages + request: + curl: | + curl https://api.openai.com/v1/threads \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "messages": [{ + "role": "user", + "content": "Hello, what is AI?" + }, { + "role": "user", + "content": "How does AI work? Explain it in simple terms." + }] + }' + python: | + from openai import OpenAI + client = OpenAI() + + message_thread = client.beta.threads.create( + messages=[ + { + "role": "user", + "content": "Hello, what is AI?" + }, + { + "role": "user", + "content": "How does AI work? Explain it in simple terms." + }, + ] + ) + + print(message_thread) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const messageThread = await openai.beta.threads.create({ + messages: [ + { + role: "user", + content: "Hello, what is AI?" + }, + { + role: "user", + content: "How does AI work? Explain it in simple terms.", + }, + ], + }); + + console.log(messageThread); + } + + main(); + response: | + { + "id": "thread_abc123", + "object": "thread", + "created_at": 1699014083, + "metadata": {}, + "tool_resources": {} + } + /threads/{thread_id}: + get: + operationId: getThread + tags: + - Assistants + summary: Retrieves a thread. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the thread to retrieve. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ThreadObject' + x-oaiMeta: + name: Retrieve thread + group: threads + beta: true + returns: The [thread](/docs/api-reference/threads/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + my_thread = client.beta.threads.retrieve("thread_abc123") + print(my_thread) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const myThread = await openai.beta.threads.retrieve( + "thread_abc123" + ); + + console.log(myThread); + } + + main(); + response: | + { + "id": "thread_abc123", + "object": "thread", + "created_at": 1699014083, + "metadata": {}, + "tool_resources": { + "code_interpreter": { + "file_ids": [] + } + } + } + post: + operationId: modifyThread + tags: + - Assistants + summary: Modifies a thread. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the thread to modify. Only the `metadata` can be modified. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyThreadRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ThreadObject' + x-oaiMeta: + name: Modify thread + group: threads + beta: true + returns: The modified [thread](/docs/api-reference/threads/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "metadata": { + "modified": "true", + "user": "abc123" + } + }' + python: | + from openai import OpenAI + client = OpenAI() + + my_updated_thread = client.beta.threads.update( + "thread_abc123", + metadata={ + "modified": "true", + "user": "abc123" + } + ) + print(my_updated_thread) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const updatedThread = await openai.beta.threads.update( + "thread_abc123", + { + metadata: { modified: "true", user: "abc123" }, + } + ); + + console.log(updatedThread); + } + + main(); + response: | + { + "id": "thread_abc123", + "object": "thread", + "created_at": 1699014083, + "metadata": { + "modified": "true", + "user": "abc123" + }, + "tool_resources": {} + } + delete: + operationId: deleteThread + tags: + - Assistants + summary: Delete a thread. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the thread to delete. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteThreadResponse' + x-oaiMeta: + name: Delete thread + group: threads + beta: true + returns: Deletion status + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -X DELETE + python: | + from openai import OpenAI + client = OpenAI() + + response = client.beta.threads.delete("thread_abc123") + print(response) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const response = await openai.beta.threads.del("thread_abc123"); + + console.log(response); + } + main(); + response: | + { + "id": "thread_abc123", + "object": "thread.deleted", + "deleted": true + } + /threads/{thread_id}/messages: + get: + operationId: listMessages + tags: + - Assistants + summary: Returns a list of messages for a given thread. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the [thread](/docs/api-reference/threads) the messages belong to. + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: order + in: query + description: | + Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + schema: + type: string + default: desc + enum: + - asc + - desc + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + schema: + type: string + - name: before + in: query + description: | + A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + schema: + type: string + - name: run_id + in: query + description: | + Filter messages by the run ID that generated them. + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListMessagesResponse' + x-oaiMeta: + name: List messages + group: threads + beta: true + returns: A list of [message](/docs/api-reference/messages) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/messages \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + thread_messages = client.beta.threads.messages.list("thread_abc123") + print(thread_messages.data) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const threadMessages = await openai.beta.threads.messages.list( + "thread_abc123" + ); + + console.log(threadMessages.data); + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699016383, + "assistant_id": null, + "thread_id": "thread_abc123", + "run_id": null, + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "attachments": [], + "metadata": {} + }, + { + "id": "msg_abc456", + "object": "thread.message", + "created_at": 1699016383, + "assistant_id": null, + "thread_id": "thread_abc123", + "run_id": null, + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "Hello, what is AI?", + "annotations": [] + } + } + ], + "attachments": [], + "metadata": {} + } + ], + "first_id": "msg_abc123", + "last_id": "msg_abc456", + "has_more": false + } + post: + operationId: createMessage + tags: + - Assistants + summary: Create a message. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the [thread](/docs/api-reference/threads) to create a message for. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMessageRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MessageObject' + x-oaiMeta: + name: Create message + group: threads + beta: true + returns: A [message](/docs/api-reference/messages/object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/messages \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "role": "user", + "content": "How does AI work? Explain it in simple terms." + }' + python: | + from openai import OpenAI + client = OpenAI() + + thread_message = client.beta.threads.messages.create( + "thread_abc123", + role="user", + content="How does AI work? Explain it in simple terms.", + ) + print(thread_message) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const threadMessages = await openai.beta.threads.messages.create( + "thread_abc123", + { role: "user", content: "How does AI work? Explain it in simple terms." } + ); + + console.log(threadMessages); + } + + main(); + response: | + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1713226573, + "assistant_id": null, + "thread_id": "thread_abc123", + "run_id": null, + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "attachments": [], + "metadata": {} + } + /threads/{thread_id}/messages/{message_id}: + get: + operationId: getMessage + tags: + - Assistants + summary: Retrieve a message. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the [thread](/docs/api-reference/threads) to which this message belongs. + - in: path + name: message_id + required: true + schema: + type: string + description: The ID of the message to retrieve. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MessageObject' + x-oaiMeta: + name: Retrieve message + group: threads + beta: true + returns: The [message](/docs/api-reference/messages/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/messages/msg_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + message = client.beta.threads.messages.retrieve( + message_id="msg_abc123", + thread_id="thread_abc123", + ) + print(message) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const message = await openai.beta.threads.messages.retrieve( + "thread_abc123", + "msg_abc123" + ); + + console.log(message); + } + + main(); + response: | + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "assistant_id": null, + "thread_id": "thread_abc123", + "run_id": null, + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "attachments": [], + "metadata": {} + } + post: + operationId: modifyMessage + tags: + - Assistants + summary: Modifies a message. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the thread to which this message belongs. + - in: path + name: message_id + required: true + schema: + type: string + description: The ID of the message to modify. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyMessageRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MessageObject' + x-oaiMeta: + name: Modify message + group: threads + beta: true + returns: The modified [message](/docs/api-reference/messages/object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/messages/msg_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "metadata": { + "modified": "true", + "user": "abc123" + } + }' + python: | + from openai import OpenAI + client = OpenAI() + + message = client.beta.threads.messages.update( + message_id="msg_abc12", + thread_id="thread_abc123", + metadata={ + "modified": "true", + "user": "abc123", + }, + ) + print(message) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const message = await openai.beta.threads.messages.update( + "thread_abc123", + "msg_abc123", + { + metadata: { + modified: "true", + user: "abc123", + }, + } + }' + response: | + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "assistant_id": null, + "thread_id": "thread_abc123", + "run_id": null, + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "metadata": { + "modified": "true", + "user": "abc123" + } + } + delete: + operationId: deleteMessage + tags: + - Assistants + summary: Deletes a message. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the thread to which this message belongs. + - in: path + name: message_id + required: true + schema: + type: string + description: The ID of the message to delete. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteMessageResponse' + x-oaiMeta: + name: Delete message + group: threads + beta: true + returns: Deletion status + examples: + request: + curl: | + curl -X DELETE https://api.openai.com/v1/threads/thread_abc123/messages/msg_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + deleted_message = client.beta.threads.messages.delete( + message_id="msg_abc12", + thread_id="thread_abc123", + ) + print(deleted_message) + node.js: |- + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const deletedMessage = await openai.beta.threads.messages.del( + "thread_abc123", + "msg_abc123" + ); + + console.log(deletedMessage); + } + response: | + { + "id": "msg_abc123", + "object": "thread.message.deleted", + "deleted": true + } + /threads/runs: + post: + operationId: createThreadAndRun + tags: + - Assistants + summary: Create a thread and run it in one request. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateThreadAndRunRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RunObject' + x-oaiMeta: + name: Create thread and run + group: threads + beta: true + returns: A [run](/docs/api-reference/runs/object) object. + examples: + - title: Default + request: + curl: | + curl https://api.openai.com/v1/threads/runs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "assistant_id": "asst_abc123", + "thread": { + "messages": [ + {"role": "user", "content": "Explain deep learning to a 5 year old."} + ] + } + }' + python: | + from openai import OpenAI + client = OpenAI() + + run = client.beta.threads.create_and_run( + assistant_id="asst_abc123", + thread={ + "messages": [ + {"role": "user", "content": "Explain deep learning to a 5 year old."} + ] + } + ) + + print(run) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const run = await openai.beta.threads.createAndRun({ + assistant_id: "asst_abc123", + thread: { + messages: [ + { role: "user", content: "Explain deep learning to a 5 year old." }, + ], + }, + }); + + console.log(run); + } + + main(); + response: | + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699076792, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "queued", + "started_at": null, + "expires_at": 1699077392, + "cancelled_at": null, + "failed_at": null, + "completed_at": null, + "required_action": null, + "last_error": null, + "model": "gpt-4o", + "instructions": "You are a helpful assistant.", + "tools": [], + "tool_resources": {}, + "metadata": {}, + "temperature": 1.0, + "top_p": 1.0, + "max_completion_tokens": null, + "max_prompt_tokens": null, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "incomplete_details": null, + "usage": null, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + } + - title: Streaming + request: + curl: | + curl https://api.openai.com/v1/threads/runs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "assistant_id": "asst_123", + "thread": { + "messages": [ + {"role": "user", "content": "Hello"} + ] + }, + "stream": true + }' + python: | + from openai import OpenAI + client = OpenAI() + + stream = client.beta.threads.create_and_run( + assistant_id="asst_123", + thread={ + "messages": [ + {"role": "user", "content": "Hello"} + ] + }, + stream=True + ) + + for event in stream: + print(event) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const stream = await openai.beta.threads.createAndRun({ + assistant_id: "asst_123", + thread: { + messages: [ + { role: "user", content: "Hello" }, + ], + }, + stream: true + }); + + for await (const event of stream) { + console.log(event); + } + } + + main(); + response: | + event: thread.created + data: {"id":"thread_123","object":"thread","created_at":1710348075,"metadata":{}} + + event: thread.run.created + data: {"id":"run_123","object":"thread.run","created_at":1710348075,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":null,"expires_at":1710348675,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"tool_resources":{},"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} + + event: thread.run.queued + data: {"id":"run_123","object":"thread.run","created_at":1710348075,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":null,"expires_at":1710348675,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"tool_resources":{},"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} + + event: thread.run.in_progress + data: {"id":"run_123","object":"thread.run","created_at":1710348075,"assistant_id":"asst_123","thread_id":"thread_123","status":"in_progress","started_at":null,"expires_at":1710348675,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"tool_resources":{},"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} + + event: thread.run.step.created + data: {"id":"step_001","object":"thread.run.step","created_at":1710348076,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710348675,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":null} + + event: thread.run.step.in_progress + data: {"id":"step_001","object":"thread.run.step","created_at":1710348076,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710348675,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":null} + + event: thread.message.created + data: {"id":"msg_001","object":"thread.message","created_at":1710348076,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[], "metadata":{}} + + event: thread.message.in_progress + data: {"id":"msg_001","object":"thread.message","created_at":1710348076,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[], "metadata":{}} + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"Hello","annotations":[]}}]}} + + ... + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" today"}}]}} + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"?"}}]}} + + event: thread.message.completed + data: {"id":"msg_001","object":"thread.message","created_at":1710348076,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"completed","incomplete_details":null,"incomplete_at":null,"completed_at":1710348077,"role":"assistant","content":[{"type":"text","text":{"value":"Hello! How can I assist you today?","annotations":[]}}], "metadata":{}} + + event: thread.run.step.completed + data: {"id":"step_001","object":"thread.run.step","created_at":1710348076,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"completed","cancelled_at":null,"completed_at":1710348077,"expires_at":1710348675,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":{"prompt_tokens":20,"completion_tokens":11,"total_tokens":31}} + + event: thread.run.completed + {"id":"run_123","object":"thread.run","created_at":1710348076,"assistant_id":"asst_123","thread_id":"thread_123","status":"completed","started_at":1713226836,"expires_at":null,"cancelled_at":null,"failed_at":null,"completed_at":1713226837,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":{"prompt_tokens":345,"completion_tokens":11,"total_tokens":356},"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} + + event: done + data: [DONE] + - title: Streaming with Functions + request: + curl: | + curl https://api.openai.com/v1/threads/runs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "assistant_id": "asst_abc123", + "thread": { + "messages": [ + {"role": "user", "content": "What is the weather like in San Francisco?"} + ] + }, + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } + } + ], + "stream": true + }' + python: | + from openai import OpenAI + client = OpenAI() + + tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + }, + } + } + ] + + stream = client.beta.threads.create_and_run( + thread={ + "messages": [ + {"role": "user", "content": "What is the weather like in San Francisco?"} + ] + }, + assistant_id="asst_abc123", + tools=tools, + stream=True + ) + + for event in stream: + print(event) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + const tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + }, + } + } + ]; + + async function main() { + const stream = await openai.beta.threads.createAndRun({ + assistant_id: "asst_123", + thread: { + messages: [ + { role: "user", content: "What is the weather like in San Francisco?" }, + ], + }, + tools: tools, + stream: true + }); + + for await (const event of stream) { + console.log(event); + } + } + + main(); + response: | + event: thread.created + data: {"id":"thread_123","object":"thread","created_at":1710351818,"metadata":{}} + + event: thread.run.created + data: {"id":"run_123","object":"thread.run","created_at":1710351818,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":null,"expires_at":1710352418,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.queued + data: {"id":"run_123","object":"thread.run","created_at":1710351818,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":null,"expires_at":1710352418,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.in_progress + data: {"id":"run_123","object":"thread.run","created_at":1710351818,"assistant_id":"asst_123","thread_id":"thread_123","status":"in_progress","started_at":1710351818,"expires_at":1710352418,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.step.created + data: {"id":"step_001","object":"thread.run.step","created_at":1710351819,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"tool_calls","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710352418,"failed_at":null,"last_error":null,"step_details":{"type":"tool_calls","tool_calls":[]},"usage":null} + + event: thread.run.step.in_progress + data: {"id":"step_001","object":"thread.run.step","created_at":1710351819,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"tool_calls","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710352418,"failed_at":null,"last_error":null,"step_details":{"type":"tool_calls","tool_calls":[]},"usage":null} + + event: thread.run.step.delta + data: {"id":"step_001","object":"thread.run.step.delta","delta":{"step_details":{"type":"tool_calls","tool_calls":[{"index":0,"id":"call_XXNp8YGaFrjrSjgqxtC8JJ1B","type":"function","function":{"name":"get_current_weather","arguments":"","output":null}}]}}} + + event: thread.run.step.delta + data: {"id":"step_001","object":"thread.run.step.delta","delta":{"step_details":{"type":"tool_calls","tool_calls":[{"index":0,"type":"function","function":{"arguments":"{\""}}]}}} + + event: thread.run.step.delta + data: {"id":"step_001","object":"thread.run.step.delta","delta":{"step_details":{"type":"tool_calls","tool_calls":[{"index":0,"type":"function","function":{"arguments":"location"}}]}}} + + ... + + event: thread.run.step.delta + data: {"id":"step_001","object":"thread.run.step.delta","delta":{"step_details":{"type":"tool_calls","tool_calls":[{"index":0,"type":"function","function":{"arguments":"ahrenheit"}}]}}} + + event: thread.run.step.delta + data: {"id":"step_001","object":"thread.run.step.delta","delta":{"step_details":{"type":"tool_calls","tool_calls":[{"index":0,"type":"function","function":{"arguments":"\"}"}}]}}} + + event: thread.run.requires_action + data: {"id":"run_123","object":"thread.run","created_at":1710351818,"assistant_id":"asst_123","thread_id":"thread_123","status":"requires_action","started_at":1710351818,"expires_at":1710352418,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":{"type":"submit_tool_outputs","submit_tool_outputs":{"tool_calls":[{"id":"call_XXNp8YGaFrjrSjgqxtC8JJ1B","type":"function","function":{"name":"get_current_weather","arguments":"{\"location\":\"San Francisco, CA\",\"unit\":\"fahrenheit\"}"}}]}},"last_error":null,"model":"gpt-4o","instructions":null,"tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":{"prompt_tokens":345,"completion_tokens":11,"total_tokens":356},"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: done + data: [DONE] + /threads/{thread_id}/runs: + get: + operationId: listRuns + tags: + - Assistants + summary: Returns a list of runs belonging to a thread. + parameters: + - name: thread_id + in: path + required: true + schema: + type: string + description: The ID of the thread the run belongs to. + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: order + in: query + description: | + Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + schema: + type: string + default: desc + enum: + - asc + - desc + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + schema: + type: string + - name: before + in: query + description: | + A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListRunsResponse' + x-oaiMeta: + name: List runs + group: threads + beta: true + returns: A list of [run](/docs/api-reference/runs/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/runs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + runs = client.beta.threads.runs.list( + "thread_abc123" + ) + + print(runs) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const runs = await openai.beta.threads.runs.list( + "thread_abc123" + ); + + console.log(runs); + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699075072, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "completed", + "started_at": 1699075072, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699075073, + "last_error": null, + "model": "gpt-4o", + "instructions": null, + "incomplete_details": null, + "tools": [ + { + "type": "code_interpreter" + } + ], + "tool_resources": { + "code_interpreter": { + "file_ids": [ + "file-abc123", + "file-abc456" + ] + } + }, + "metadata": {}, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + }, + "temperature": 1.0, + "top_p": 1.0, + "max_prompt_tokens": 1000, + "max_completion_tokens": 1000, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + }, + { + "id": "run_abc456", + "object": "thread.run", + "created_at": 1699063290, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "completed", + "started_at": 1699063290, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699063291, + "last_error": null, + "model": "gpt-4o", + "instructions": null, + "incomplete_details": null, + "tools": [ + { + "type": "code_interpreter" + } + ], + "tool_resources": { + "code_interpreter": { + "file_ids": [ + "file-abc123", + "file-abc456" + ] + } + }, + "metadata": {}, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + }, + "temperature": 1.0, + "top_p": 1.0, + "max_prompt_tokens": 1000, + "max_completion_tokens": 1000, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + } + ], + "first_id": "run_abc123", + "last_id": "run_abc456", + "has_more": false + } + post: + operationId: createRun + tags: + - Assistants + summary: Create a run. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the thread to run. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateRunRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RunObject' + x-oaiMeta: + name: Create run + group: threads + beta: true + returns: A [run](/docs/api-reference/runs/object) object. + examples: + - title: Default + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/runs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "assistant_id": "asst_abc123" + }' + python: | + from openai import OpenAI + client = OpenAI() + + run = client.beta.threads.runs.create( + thread_id="thread_abc123", + assistant_id="asst_abc123" + ) + + print(run) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const run = await openai.beta.threads.runs.create( + "thread_abc123", + { assistant_id: "asst_abc123" } + ); + + console.log(run); + } + + main(); + response: | + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699063290, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "queued", + "started_at": 1699063290, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699063291, + "last_error": null, + "model": "gpt-4o", + "instructions": null, + "incomplete_details": null, + "tools": [ + { + "type": "code_interpreter" + } + ], + "metadata": {}, + "usage": null, + "temperature": 1.0, + "top_p": 1.0, + "max_prompt_tokens": 1000, + "max_completion_tokens": 1000, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + } + - title: Streaming + request: + curl: | + curl https://api.openai.com/v1/threads/thread_123/runs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "assistant_id": "asst_123", + "stream": true + }' + python: | + from openai import OpenAI + client = OpenAI() + + stream = client.beta.threads.runs.create( + thread_id="thread_123", + assistant_id="asst_123", + stream=True + ) + + for event in stream: + print(event) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const stream = await openai.beta.threads.runs.create( + "thread_123", + { assistant_id: "asst_123", stream: true } + ); + + for await (const event of stream) { + console.log(event); + } + } + + main(); + response: | + event: thread.run.created + data: {"id":"run_123","object":"thread.run","created_at":1710330640,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":null,"expires_at":1710331240,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.queued + data: {"id":"run_123","object":"thread.run","created_at":1710330640,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":null,"expires_at":1710331240,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.in_progress + data: {"id":"run_123","object":"thread.run","created_at":1710330640,"assistant_id":"asst_123","thread_id":"thread_123","status":"in_progress","started_at":1710330641,"expires_at":1710331240,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.step.created + data: {"id":"step_001","object":"thread.run.step","created_at":1710330641,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710331240,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":null} + + event: thread.run.step.in_progress + data: {"id":"step_001","object":"thread.run.step","created_at":1710330641,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710331240,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":null} + + event: thread.message.created + data: {"id":"msg_001","object":"thread.message","created_at":1710330641,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"metadata":{}} + + event: thread.message.in_progress + data: {"id":"msg_001","object":"thread.message","created_at":1710330641,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"metadata":{}} + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"Hello","annotations":[]}}]}} + + ... + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" today"}}]}} + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"?"}}]}} + + event: thread.message.completed + data: {"id":"msg_001","object":"thread.message","created_at":1710330641,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"completed","incomplete_details":null,"incomplete_at":null,"completed_at":1710330642,"role":"assistant","content":[{"type":"text","text":{"value":"Hello! How can I assist you today?","annotations":[]}}],"metadata":{}} + + event: thread.run.step.completed + data: {"id":"step_001","object":"thread.run.step","created_at":1710330641,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"completed","cancelled_at":null,"completed_at":1710330642,"expires_at":1710331240,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":{"prompt_tokens":20,"completion_tokens":11,"total_tokens":31}} + + event: thread.run.completed + data: {"id":"run_123","object":"thread.run","created_at":1710330640,"assistant_id":"asst_123","thread_id":"thread_123","status":"completed","started_at":1710330641,"expires_at":null,"cancelled_at":null,"failed_at":null,"completed_at":1710330642,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":{"prompt_tokens":20,"completion_tokens":11,"total_tokens":31},"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: done + data: [DONE] + - title: Streaming with Functions + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/runs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "assistant_id": "asst_abc123", + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } + } + ], + "stream": true + }' + python: | + from openai import OpenAI + client = OpenAI() + + tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + }, + } + } + ] + + stream = client.beta.threads.runs.create( + thread_id="thread_abc123", + assistant_id="asst_abc123", + tools=tools, + stream=True + ) + + for event in stream: + print(event) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + const tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + }, + } + } + ]; + + async function main() { + const stream = await openai.beta.threads.runs.create( + "thread_abc123", + { + assistant_id: "asst_abc123", + tools: tools, + stream: true + } + ); + + for await (const event of stream) { + console.log(event); + } + } + + main(); + response: | + event: thread.run.created + data: {"id":"run_123","object":"thread.run","created_at":1710348075,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":null,"expires_at":1710348675,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.queued + data: {"id":"run_123","object":"thread.run","created_at":1710348075,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":null,"expires_at":1710348675,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.in_progress + data: {"id":"run_123","object":"thread.run","created_at":1710348075,"assistant_id":"asst_123","thread_id":"thread_123","status":"in_progress","started_at":1710348075,"expires_at":1710348675,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.step.created + data: {"id":"step_001","object":"thread.run.step","created_at":1710348076,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710348675,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":null} + + event: thread.run.step.in_progress + data: {"id":"step_001","object":"thread.run.step","created_at":1710348076,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710348675,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":null} + + event: thread.message.created + data: {"id":"msg_001","object":"thread.message","created_at":1710348076,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"metadata":{}} + + event: thread.message.in_progress + data: {"id":"msg_001","object":"thread.message","created_at":1710348076,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"metadata":{}} + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"Hello","annotations":[]}}]}} + + ... + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" today"}}]}} + + event: thread.message.delta + data: {"id":"msg_001","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"?"}}]}} + + event: thread.message.completed + data: {"id":"msg_001","object":"thread.message","created_at":1710348076,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"completed","incomplete_details":null,"incomplete_at":null,"completed_at":1710348077,"role":"assistant","content":[{"type":"text","text":{"value":"Hello! How can I assist you today?","annotations":[]}}],"metadata":{}} + + event: thread.run.step.completed + data: {"id":"step_001","object":"thread.run.step","created_at":1710348076,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"completed","cancelled_at":null,"completed_at":1710348077,"expires_at":1710348675,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_001"}},"usage":{"prompt_tokens":20,"completion_tokens":11,"total_tokens":31}} + + event: thread.run.completed + data: {"id":"run_123","object":"thread.run","created_at":1710348075,"assistant_id":"asst_123","thread_id":"thread_123","status":"completed","started_at":1710348075,"expires_at":null,"cancelled_at":null,"failed_at":null,"completed_at":1710348077,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":{"prompt_tokens":20,"completion_tokens":11,"total_tokens":31},"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: done + data: [DONE] + /threads/{thread_id}/runs/{run_id}: + get: + operationId: getRun + tags: + - Assistants + summary: Retrieves a run. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the [thread](/docs/api-reference/threads) that was run. + - in: path + name: run_id + required: true + schema: + type: string + description: The ID of the run to retrieve. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RunObject' + x-oaiMeta: + name: Retrieve run + group: threads + beta: true + returns: The [run](/docs/api-reference/runs/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/runs/run_abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + run = client.beta.threads.runs.retrieve( + thread_id="thread_abc123", + run_id="run_abc123" + ) + + print(run) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const run = await openai.beta.threads.runs.retrieve( + "thread_abc123", + "run_abc123" + ); + + console.log(run); + } + + main(); + response: | + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699075072, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "completed", + "started_at": 1699075072, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699075073, + "last_error": null, + "model": "gpt-4o", + "instructions": null, + "incomplete_details": null, + "tools": [ + { + "type": "code_interpreter" + } + ], + "metadata": {}, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + }, + "temperature": 1.0, + "top_p": 1.0, + "max_prompt_tokens": 1000, + "max_completion_tokens": 1000, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + } + post: + operationId: modifyRun + tags: + - Assistants + summary: Modifies a run. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the [thread](/docs/api-reference/threads) that was run. + - in: path + name: run_id + required: true + schema: + type: string + description: The ID of the run to modify. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyRunRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RunObject' + x-oaiMeta: + name: Modify run + group: threads + beta: true + returns: The modified [run](/docs/api-reference/runs/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/runs/run_abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "metadata": { + "user_id": "user_abc123" + } + }' + python: | + from openai import OpenAI + client = OpenAI() + + run = client.beta.threads.runs.update( + thread_id="thread_abc123", + run_id="run_abc123", + metadata={"user_id": "user_abc123"}, + ) + + print(run) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const run = await openai.beta.threads.runs.update( + "thread_abc123", + "run_abc123", + { + metadata: { + user_id: "user_abc123", + }, + } + ); + + console.log(run); + } + + main(); + response: | + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699075072, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "completed", + "started_at": 1699075072, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699075073, + "last_error": null, + "model": "gpt-4o", + "instructions": null, + "incomplete_details": null, + "tools": [ + { + "type": "code_interpreter" + } + ], + "tool_resources": { + "code_interpreter": { + "file_ids": [ + "file-abc123", + "file-abc456" + ] + } + }, + "metadata": { + "user_id": "user_abc123" + }, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + }, + "temperature": 1.0, + "top_p": 1.0, + "max_prompt_tokens": 1000, + "max_completion_tokens": 1000, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + } + /threads/{thread_id}/runs/{run_id}/submit_tool_outputs: + post: + operationId: submitToolOuputsToRun + tags: + - Assistants + summary: | + When a run has the `status: "requires_action"` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the [thread](/docs/api-reference/threads) to which this run belongs. + - in: path + name: run_id + required: true + schema: + type: string + description: The ID of the run that requires the tool output submission. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SubmitToolOutputsRunRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RunObject' + x-oaiMeta: + name: Submit tool outputs to run + group: threads + beta: true + returns: The modified [run](/docs/api-reference/runs/object) object matching the specified ID. + examples: + - title: Default + request: + curl: | + curl https://api.openai.com/v1/threads/thread_123/runs/run_123/submit_tool_outputs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "tool_outputs": [ + { + "tool_call_id": "call_001", + "output": "70 degrees and sunny." + } + ] + }' + python: | + from openai import OpenAI + client = OpenAI() + + run = client.beta.threads.runs.submit_tool_outputs( + thread_id="thread_123", + run_id="run_123", + tool_outputs=[ + { + "tool_call_id": "call_001", + "output": "70 degrees and sunny." + } + ] + ) + + print(run) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const run = await openai.beta.threads.runs.submitToolOutputs( + "thread_123", + "run_123", + { + tool_outputs: [ + { + tool_call_id: "call_001", + output: "70 degrees and sunny.", + }, + ], + } + ); + + console.log(run); + } + + main(); + response: | + { + "id": "run_123", + "object": "thread.run", + "created_at": 1699075592, + "assistant_id": "asst_123", + "thread_id": "thread_123", + "status": "queued", + "started_at": 1699075592, + "expires_at": 1699076192, + "cancelled_at": null, + "failed_at": null, + "completed_at": null, + "last_error": null, + "model": "gpt-4o", + "instructions": null, + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } + } + ], + "metadata": {}, + "usage": null, + "temperature": 1.0, + "top_p": 1.0, + "max_prompt_tokens": 1000, + "max_completion_tokens": 1000, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + } + - title: Streaming + request: + curl: | + curl https://api.openai.com/v1/threads/thread_123/runs/run_123/submit_tool_outputs \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "tool_outputs": [ + { + "tool_call_id": "call_001", + "output": "70 degrees and sunny." + } + ], + "stream": true + }' + python: | + from openai import OpenAI + client = OpenAI() + + stream = client.beta.threads.runs.submit_tool_outputs( + thread_id="thread_123", + run_id="run_123", + tool_outputs=[ + { + "tool_call_id": "call_001", + "output": "70 degrees and sunny." + } + ], + stream=True + ) + + for event in stream: + print(event) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const stream = await openai.beta.threads.runs.submitToolOutputs( + "thread_123", + "run_123", + { + tool_outputs: [ + { + tool_call_id: "call_001", + output: "70 degrees and sunny.", + }, + ], + } + ); + + for await (const event of stream) { + console.log(event); + } + } + + main(); + response: | + event: thread.run.step.completed + data: {"id":"step_001","object":"thread.run.step","created_at":1710352449,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"tool_calls","status":"completed","cancelled_at":null,"completed_at":1710352475,"expires_at":1710353047,"failed_at":null,"last_error":null,"step_details":{"type":"tool_calls","tool_calls":[{"id":"call_iWr0kQ2EaYMaxNdl0v3KYkx7","type":"function","function":{"name":"get_current_weather","arguments":"{\"location\":\"San Francisco, CA\",\"unit\":\"fahrenheit\"}","output":"70 degrees and sunny."}}]},"usage":{"prompt_tokens":291,"completion_tokens":24,"total_tokens":315}} + + event: thread.run.queued + data: {"id":"run_123","object":"thread.run","created_at":1710352447,"assistant_id":"asst_123","thread_id":"thread_123","status":"queued","started_at":1710352448,"expires_at":1710353047,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.in_progress + data: {"id":"run_123","object":"thread.run","created_at":1710352447,"assistant_id":"asst_123","thread_id":"thread_123","status":"in_progress","started_at":1710352475,"expires_at":1710353047,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: thread.run.step.created + data: {"id":"step_002","object":"thread.run.step","created_at":1710352476,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710353047,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_002"}},"usage":null} + + event: thread.run.step.in_progress + data: {"id":"step_002","object":"thread.run.step","created_at":1710352476,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1710353047,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_002"}},"usage":null} + + event: thread.message.created + data: {"id":"msg_002","object":"thread.message","created_at":1710352476,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"metadata":{}} + + event: thread.message.in_progress + data: {"id":"msg_002","object":"thread.message","created_at":1710352476,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"metadata":{}} + + event: thread.message.delta + data: {"id":"msg_002","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"The","annotations":[]}}]}} + + event: thread.message.delta + data: {"id":"msg_002","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" current"}}]}} + + event: thread.message.delta + data: {"id":"msg_002","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" weather"}}]}} + + ... + + event: thread.message.delta + data: {"id":"msg_002","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" sunny"}}]}} + + event: thread.message.delta + data: {"id":"msg_002","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"."}}]}} + + event: thread.message.completed + data: {"id":"msg_002","object":"thread.message","created_at":1710352476,"assistant_id":"asst_123","thread_id":"thread_123","run_id":"run_123","status":"completed","incomplete_details":null,"incomplete_at":null,"completed_at":1710352477,"role":"assistant","content":[{"type":"text","text":{"value":"The current weather in San Francisco, CA is 70 degrees Fahrenheit and sunny.","annotations":[]}}],"metadata":{}} + + event: thread.run.step.completed + data: {"id":"step_002","object":"thread.run.step","created_at":1710352476,"run_id":"run_123","assistant_id":"asst_123","thread_id":"thread_123","type":"message_creation","status":"completed","cancelled_at":null,"completed_at":1710352477,"expires_at":1710353047,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_002"}},"usage":{"prompt_tokens":329,"completion_tokens":18,"total_tokens":347}} + + event: thread.run.completed + data: {"id":"run_123","object":"thread.run","created_at":1710352447,"assistant_id":"asst_123","thread_id":"thread_123","status":"completed","started_at":1710352475,"expires_at":null,"cancelled_at":null,"failed_at":null,"completed_at":1710352477,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":null,"tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}],"metadata":{},"temperature":1.0,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":{"prompt_tokens":20,"completion_tokens":11,"total_tokens":31},"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true}} + + event: done + data: [DONE] + /threads/{thread_id}/runs/{run_id}/cancel: + post: + operationId: cancelRun + tags: + - Assistants + summary: Cancels a run that is `in_progress`. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the thread to which this run belongs. + - in: path + name: run_id + required: true + schema: + type: string + description: The ID of the run to cancel. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RunObject' + x-oaiMeta: + name: Cancel a run + group: threads + beta: true + returns: The modified [run](/docs/api-reference/runs/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/runs/run_abc123/cancel \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "OpenAI-Beta: assistants=v2" \ + -X POST + python: | + from openai import OpenAI + client = OpenAI() + + run = client.beta.threads.runs.cancel( + thread_id="thread_abc123", + run_id="run_abc123" + ) + + print(run) + node.js: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const run = await openai.beta.threads.runs.cancel( + "thread_abc123", + "run_abc123" + ); + + console.log(run); + } + + main(); + response: | + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699076126, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "cancelling", + "started_at": 1699076126, + "expires_at": 1699076726, + "cancelled_at": null, + "failed_at": null, + "completed_at": null, + "last_error": null, + "model": "gpt-4o", + "instructions": "You summarize books.", + "tools": [ + { + "type": "file_search" + } + ], + "tool_resources": { + "file_search": { + "vector_store_ids": ["vs_123"] + } + }, + "metadata": {}, + "usage": null, + "temperature": 1.0, + "top_p": 1.0, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + } + /threads/{thread_id}/runs/{run_id}/steps: + get: + operationId: listRunSteps + tags: + - Assistants + summary: Returns a list of run steps belonging to a run. + parameters: + - name: thread_id + in: path + required: true + schema: + type: string + description: The ID of the thread the run and run steps belong to. + - name: run_id + in: path + required: true + schema: + type: string + description: The ID of the run the run steps belong to. + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: order + in: query + description: | + Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + schema: + type: string + default: desc + enum: + - asc + - desc + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + schema: + type: string + - name: before + in: query + description: | + A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListRunStepsResponse' + x-oaiMeta: + name: List run steps + group: threads + beta: true + returns: A list of [run step](/docs/api-reference/runs/step-object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/runs/run_abc123/steps \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + run_steps = client.beta.threads.runs.steps.list( + thread_id="thread_abc123", + run_id="run_abc123" + ) + + print(run_steps) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const runStep = await openai.beta.threads.runs.steps.list( + "thread_abc123", + "run_abc123" + ); + console.log(runStep); + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "step_abc123", + "object": "thread.run.step", + "created_at": 1699063291, + "run_id": "run_abc123", + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "type": "message_creation", + "status": "completed", + "cancelled_at": null, + "completed_at": 1699063291, + "expired_at": null, + "failed_at": null, + "last_error": null, + "step_details": { + "type": "message_creation", + "message_creation": { + "message_id": "msg_abc123" + } + }, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + } + } + ], + "first_id": "step_abc123", + "last_id": "step_abc456", + "has_more": false + } + /threads/{thread_id}/runs/{run_id}/steps/{step_id}: + get: + operationId: getRunStep + tags: + - Assistants + summary: Retrieves a run step. + parameters: + - in: path + name: thread_id + required: true + schema: + type: string + description: The ID of the thread to which the run and run step belongs. + - in: path + name: run_id + required: true + schema: + type: string + description: The ID of the run to which the run step belongs. + - in: path + name: step_id + required: true + schema: + type: string + description: The ID of the run step to retrieve. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RunStepObject' + x-oaiMeta: + name: Retrieve run step + group: threads + beta: true + returns: The [run step](/docs/api-reference/runs/step-object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/threads/thread_abc123/runs/run_abc123/steps/step_abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + run_step = client.beta.threads.runs.steps.retrieve( + thread_id="thread_abc123", + run_id="run_abc123", + step_id="step_abc123" + ) + + print(run_step) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const runStep = await openai.beta.threads.runs.steps.retrieve( + "thread_abc123", + "run_abc123", + "step_abc123" + ); + console.log(runStep); + } + + main(); + response: | + { + "id": "step_abc123", + "object": "thread.run.step", + "created_at": 1699063291, + "run_id": "run_abc123", + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "type": "message_creation", + "status": "completed", + "cancelled_at": null, + "completed_at": 1699063291, + "expired_at": null, + "failed_at": null, + "last_error": null, + "step_details": { + "type": "message_creation", + "message_creation": { + "message_id": "msg_abc123" + } + }, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + } + } + /vector_stores: + get: + operationId: listVectorStores + tags: + - Vector Stores + summary: Returns a list of vector stores. + parameters: + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: order + in: query + description: | + Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + schema: + type: string + default: desc + enum: + - asc + - desc + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + schema: + type: string + - name: before + in: query + description: | + A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListVectorStoresResponse' + x-oaiMeta: + name: List vector stores + group: vector_stores + beta: true + returns: A list of [vector store](/docs/api-reference/vector-stores/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + vector_stores = client.beta.vector_stores.list() + print(vector_stores) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const vectorStores = await openai.beta.vectorStores.list(); + console.log(vectorStores); + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "vs_abc123", + "object": "vector_store", + "created_at": 1699061776, + "name": "Support FAQ", + "bytes": 139920, + "file_counts": { + "in_progress": 0, + "completed": 3, + "failed": 0, + "cancelled": 0, + "total": 3 + } + }, + { + "id": "vs_abc456", + "object": "vector_store", + "created_at": 1699061776, + "name": "Support FAQ v2", + "bytes": 139920, + "file_counts": { + "in_progress": 0, + "completed": 3, + "failed": 0, + "cancelled": 0, + "total": 3 + } + } + ], + "first_id": "vs_abc123", + "last_id": "vs_abc456", + "has_more": false + } + post: + operationId: createVectorStore + tags: + - Vector Stores + summary: Create a vector store. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateVectorStoreRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VectorStoreObject' + x-oaiMeta: + name: Create vector store + group: vector_stores + beta: true + returns: A [vector store](/docs/api-reference/vector-stores/object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + -d '{ + "name": "Support FAQ" + }' + python: | + from openai import OpenAI + client = OpenAI() + + vector_store = client.beta.vector_stores.create( + name="Support FAQ" + ) + print(vector_store) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const vectorStore = await openai.beta.vectorStores.create({ + name: "Support FAQ" + }); + console.log(vectorStore); + } + + main(); + response: | + { + "id": "vs_abc123", + "object": "vector_store", + "created_at": 1699061776, + "name": "Support FAQ", + "bytes": 139920, + "file_counts": { + "in_progress": 0, + "completed": 3, + "failed": 0, + "cancelled": 0, + "total": 3 + } + } + /vector_stores/{vector_store_id}: + get: + operationId: getVectorStore + tags: + - Vector Stores + summary: Retrieves a vector store. + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + description: The ID of the vector store to retrieve. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VectorStoreObject' + x-oaiMeta: + name: Retrieve vector store + group: vector_stores + beta: true + returns: The [vector store](/docs/api-reference/vector-stores/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + vector_store = client.beta.vector_stores.retrieve( + vector_store_id="vs_abc123" + ) + print(vector_store) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const vectorStore = await openai.beta.vectorStores.retrieve( + "vs_abc123" + ); + console.log(vectorStore); + } + + main(); + response: | + { + "id": "vs_abc123", + "object": "vector_store", + "created_at": 1699061776 + } + post: + operationId: modifyVectorStore + tags: + - Vector Stores + summary: Modifies a vector store. + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + description: The ID of the vector store to modify. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateVectorStoreRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VectorStoreObject' + x-oaiMeta: + name: Modify vector store + group: vector_stores + beta: true + returns: The modified [vector store](/docs/api-reference/vector-stores/object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + -d '{ + "name": "Support FAQ" + }' + python: | + from openai import OpenAI + client = OpenAI() + + vector_store = client.beta.vector_stores.update( + vector_store_id="vs_abc123", + name="Support FAQ" + ) + print(vector_store) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const vectorStore = await openai.beta.vectorStores.update( + "vs_abc123", + { + name: "Support FAQ" + } + ); + console.log(vectorStore); + } + + main(); + response: | + { + "id": "vs_abc123", + "object": "vector_store", + "created_at": 1699061776, + "name": "Support FAQ", + "bytes": 139920, + "file_counts": { + "in_progress": 0, + "completed": 3, + "failed": 0, + "cancelled": 0, + "total": 3 + } + } + delete: + operationId: deleteVectorStore + tags: + - Vector Stores + summary: Delete a vector store. + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + description: The ID of the vector store to delete. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteVectorStoreResponse' + x-oaiMeta: + name: Delete vector store + group: vector_stores + beta: true + returns: Deletion status + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -X DELETE + python: | + from openai import OpenAI + client = OpenAI() + + deleted_vector_store = client.beta.vector_stores.delete( + vector_store_id="vs_abc123" + ) + print(deleted_vector_store) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const deletedVectorStore = await openai.beta.vectorStores.del( + "vs_abc123" + ); + console.log(deletedVectorStore); + } + + main(); + response: | + { + id: "vs_abc123", + object: "vector_store.deleted", + deleted: true + } + /vector_stores/{vector_store_id}/files: + get: + operationId: listVectorStoreFiles + tags: + - Vector Stores + summary: Returns a list of vector store files. + parameters: + - name: vector_store_id + in: path + description: The ID of the vector store that the files belong to. + required: true + schema: + type: string + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: order + in: query + description: | + Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + schema: + type: string + default: desc + enum: + - asc + - desc + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + schema: + type: string + - name: before + in: query + description: | + A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + schema: + type: string + - name: filter + in: query + description: Filter by file status. One of `in_progress`, `completed`, `failed`, `cancelled`. + schema: + type: string + enum: + - in_progress + - completed + - failed + - cancelled + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListVectorStoreFilesResponse' + x-oaiMeta: + name: List vector store files + group: vector_stores + beta: true + returns: A list of [vector store file](/docs/api-reference/vector-stores-files/file-object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123/files \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + vector_store_files = client.beta.vector_stores.files.list( + vector_store_id="vs_abc123" + ) + print(vector_store_files) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const vectorStoreFiles = await openai.beta.vectorStores.files.list( + "vs_abc123" + ); + console.log(vectorStoreFiles); + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "file-abc123", + "object": "vector_store.file", + "created_at": 1699061776, + "vector_store_id": "vs_abc123" + }, + { + "id": "file-abc456", + "object": "vector_store.file", + "created_at": 1699061776, + "vector_store_id": "vs_abc123" + } + ], + "first_id": "file-abc123", + "last_id": "file-abc456", + "has_more": false + } + post: + operationId: createVectorStoreFile + tags: + - Vector Stores + summary: Create a vector store file by attaching a [File](/docs/api-reference/files) to a [vector store](/docs/api-reference/vector-stores/object). + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + example: vs_abc123 + description: | + The ID of the vector store for which to create a File. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateVectorStoreFileRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VectorStoreFileObject' + x-oaiMeta: + name: Create vector store file + group: vector_stores + beta: true + returns: A [vector store file](/docs/api-reference/vector-stores-files/file-object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123/files \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "file_id": "file-abc123" + }' + python: | + from openai import OpenAI + client = OpenAI() + + vector_store_file = client.beta.vector_stores.files.create( + vector_store_id="vs_abc123", + file_id="file-abc123" + ) + print(vector_store_file) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const myVectorStoreFile = await openai.beta.vectorStores.files.create( + "vs_abc123", + { + file_id: "file-abc123" + } + ); + console.log(myVectorStoreFile); + } + + main(); + response: | + { + "id": "file-abc123", + "object": "vector_store.file", + "created_at": 1699061776, + "usage_bytes": 1234, + "vector_store_id": "vs_abcd", + "status": "completed", + "last_error": null + } + /vector_stores/{vector_store_id}/files/{file_id}: + get: + operationId: getVectorStoreFile + tags: + - Vector Stores + summary: Retrieves a vector store file. + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + example: vs_abc123 + description: The ID of the vector store that the file belongs to. + - in: path + name: file_id + required: true + schema: + type: string + example: file-abc123 + description: The ID of the file being retrieved. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VectorStoreFileObject' + x-oaiMeta: + name: Retrieve vector store file + group: vector_stores + beta: true + returns: The [vector store file](/docs/api-reference/vector-stores-files/file-object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123/files/file-abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + vector_store_file = client.beta.vector_stores.files.retrieve( + vector_store_id="vs_abc123", + file_id="file-abc123" + ) + print(vector_store_file) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const vectorStoreFile = await openai.beta.vectorStores.files.retrieve( + "vs_abc123", + "file-abc123" + ); + console.log(vectorStoreFile); + } + + main(); + response: | + { + "id": "file-abc123", + "object": "vector_store.file", + "created_at": 1699061776, + "vector_store_id": "vs_abcd", + "status": "completed", + "last_error": null + } + delete: + operationId: deleteVectorStoreFile + tags: + - Vector Stores + summary: Delete a vector store file. This will remove the file from the vector store but the file itself will not be deleted. To delete the file, use the [delete file](/docs/api-reference/files/delete) endpoint. + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + description: The ID of the vector store that the file belongs to. + - in: path + name: file_id + required: true + schema: + type: string + description: The ID of the file to delete. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteVectorStoreFileResponse' + x-oaiMeta: + name: Delete vector store file + group: vector_stores + beta: true + returns: Deletion status + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123/files/file-abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -X DELETE + python: | + from openai import OpenAI + client = OpenAI() + + deleted_vector_store_file = client.beta.vector_stores.files.delete( + vector_store_id="vs_abc123", + file_id="file-abc123" + ) + print(deleted_vector_store_file) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const deletedVectorStoreFile = await openai.beta.vectorStores.files.del( + "vs_abc123", + "file-abc123" + ); + console.log(deletedVectorStoreFile); + } + + main(); + response: | + { + id: "file-abc123", + object: "vector_store.file.deleted", + deleted: true + } + /vector_stores/{vector_store_id}/file_batches: + post: + operationId: createVectorStoreFileBatch + tags: + - Vector Stores + summary: Create a vector store file batch. + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + example: vs_abc123 + description: | + The ID of the vector store for which to create a File Batch. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateVectorStoreFileBatchRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VectorStoreFileBatchObject' + x-oaiMeta: + name: Create vector store file batch + group: vector_stores + beta: true + returns: A [vector store file batch](/docs/api-reference/vector-stores-file-batches/batch-object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123/file_batches \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json \ + -H "OpenAI-Beta: assistants=v2" \ + -d '{ + "file_ids": ["file-abc123", "file-abc456"] + }' + python: | + from openai import OpenAI + client = OpenAI() + + vector_store_file_batch = client.beta.vector_stores.file_batches.create( + vector_store_id="vs_abc123", + file_ids=["file-abc123", "file-abc456"] + ) + print(vector_store_file_batch) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const myVectorStoreFileBatch = await openai.beta.vectorStores.fileBatches.create( + "vs_abc123", + { + file_ids: ["file-abc123", "file-abc456"] + } + ); + console.log(myVectorStoreFileBatch); + } + + main(); + response: | + { + "id": "vsfb_abc123", + "object": "vector_store.file_batch", + "created_at": 1699061776, + "vector_store_id": "vs_abc123", + "status": "in_progress", + "file_counts": { + "in_progress": 1, + "completed": 1, + "failed": 0, + "cancelled": 0, + "total": 0, + } + } + /vector_stores/{vector_store_id}/file_batches/{batch_id}: + get: + operationId: getVectorStoreFileBatch + tags: + - Vector Stores + summary: Retrieves a vector store file batch. + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + example: vs_abc123 + description: The ID of the vector store that the file batch belongs to. + - in: path + name: batch_id + required: true + schema: + type: string + example: vsfb_abc123 + description: The ID of the file batch being retrieved. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VectorStoreFileBatchObject' + x-oaiMeta: + name: Retrieve vector store file batch + group: vector_stores + beta: true + returns: The [vector store file batch](/docs/api-reference/vector-stores-file-batches/batch-object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123/files_batches/vsfb_abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + vector_store_file_batch = client.beta.vector_stores.file_batches.retrieve( + vector_store_id="vs_abc123", + batch_id="vsfb_abc123" + ) + print(vector_store_file_batch) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const vectorStoreFileBatch = await openai.beta.vectorStores.fileBatches.retrieve( + "vs_abc123", + "vsfb_abc123" + ); + console.log(vectorStoreFileBatch); + } + + main(); + response: | + { + "id": "vsfb_abc123", + "object": "vector_store.file_batch", + "created_at": 1699061776, + "vector_store_id": "vs_abc123", + "status": "in_progress", + "file_counts": { + "in_progress": 1, + "completed": 1, + "failed": 0, + "cancelled": 0, + "total": 0, + } + } + /vector_stores/{vector_store_id}/file_batches/{batch_id}/cancel: + post: + operationId: cancelVectorStoreFileBatch + tags: + - Vector Stores + summary: Cancel a vector store file batch. This attempts to cancel the processing of files in this batch as soon as possible. + parameters: + - in: path + name: vector_store_id + required: true + schema: + type: string + description: The ID of the vector store that the file batch belongs to. + - in: path + name: batch_id + required: true + schema: + type: string + description: The ID of the file batch to cancel. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VectorStoreFileBatchObject' + x-oaiMeta: + name: Cancel vector store file batch + group: vector_stores + beta: true + returns: The modified vector store file batch object. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123/files_batches/vsfb_abc123/cancel \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" \ + -X POST + python: | + from openai import OpenAI + client = OpenAI() + + deleted_vector_store_file_batch = client.beta.vector_stores.file_batches.cancel( + vector_store_id="vs_abc123", + file_batch_id="vsfb_abc123" + ) + print(deleted_vector_store_file_batch) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const deletedVectorStoreFileBatch = await openai.vector_stores.fileBatches.cancel( + "vs_abc123", + "vsfb_abc123" + ); + console.log(deletedVectorStoreFileBatch); + } + + main(); + response: | + { + "id": "vsfb_abc123", + "object": "vector_store.file_batch", + "created_at": 1699061776, + "vector_store_id": "vs_abc123", + "status": "cancelling", + "file_counts": { + "in_progress": 12, + "completed": 3, + "failed": 0, + "cancelled": 0, + "total": 15, + } + } + /vector_stores/{vector_store_id}/file_batches/{batch_id}/files: + get: + operationId: listFilesInVectorStoreBatch + tags: + - Vector Stores + summary: Returns a list of vector store files in a batch. + parameters: + - name: vector_store_id + in: path + description: The ID of the vector store that the files belong to. + required: true + schema: + type: string + - name: batch_id + in: path + description: The ID of the file batch that the files belong to. + required: true + schema: + type: string + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: order + in: query + description: | + Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + schema: + type: string + default: desc + enum: + - asc + - desc + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + schema: + type: string + - name: before + in: query + description: | + A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + schema: + type: string + - name: filter + in: query + description: Filter by file status. One of `in_progress`, `completed`, `failed`, `cancelled`. + schema: + type: string + enum: + - in_progress + - completed + - failed + - cancelled + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListVectorStoreFilesResponse' + x-oaiMeta: + name: List vector store files in a batch + group: vector_stores + beta: true + returns: A list of [vector store file](/docs/api-reference/vector-stores-files/file-object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/vector_stores/vs_abc123/files_batches/vsfb_abc123/files \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -H "OpenAI-Beta: assistants=v2" + python: | + from openai import OpenAI + client = OpenAI() + + vector_store_files = client.beta.vector_stores.file_batches.list_files( + vector_store_id="vs_abc123", + batch_id="vsfb_abc123" + ) + print(vector_store_files) + node.js: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + async function main() { + const vectorStoreFiles = await openai.beta.vectorStores.fileBatches.listFiles( + "vs_abc123", + "vsfb_abc123" + ); + console.log(vectorStoreFiles); + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "file-abc123", + "object": "vector_store.file", + "created_at": 1699061776, + "vector_store_id": "vs_abc123" + }, + { + "id": "file-abc456", + "object": "vector_store.file", + "created_at": 1699061776, + "vector_store_id": "vs_abc123" + } + ], + "first_id": "file-abc123", + "last_id": "file-abc456", + "has_more": false + } + /batches: + post: + summary: Creates and executes a batch from an uploaded file of requests + operationId: createBatch + tags: + - Batch + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - input_file_id + - endpoint + - completion_window + properties: + input_file_id: + type: string + description: | + The ID of an uploaded file that contains requests for the new batch. + + See [upload file](/docs/api-reference/files/create) for how to upload a file. + + Your input file must be formatted as a [JSONL file](/docs/api-reference/batch/request-input), and must be uploaded with the purpose `batch`. The file can contain up to 50,000 requests, and can be up to 100 MB in size. + endpoint: + type: string + enum: + - /v1/chat/completions + - /v1/embeddings + - /v1/completions + description: The endpoint to be used for all requests in the batch. Currently `/v1/chat/completions`, `/v1/embeddings`, and `/v1/completions` are supported. Note that `/v1/embeddings` batches are also restricted to a maximum of 50,000 embedding inputs across all requests in the batch. + completion_window: + type: string + enum: + - 24h + description: The time frame within which the batch should be processed. Currently only `24h` is supported. + metadata: + type: object + additionalProperties: + type: string + description: Optional custom metadata for the batch. + nullable: true + responses: + '200': + description: Batch created successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Batch' + x-oaiMeta: + name: Create batch + group: batch + returns: The created [Batch](/docs/api-reference/batch/object) object. + examples: + request: + curl: | + curl https://api.openai.com/v1/batches \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "input_file_id": "file-abc123", + "endpoint": "/v1/chat/completions", + "completion_window": "24h" + }' + python: | + from openai import OpenAI + client = OpenAI() + + client.batches.create( + input_file_id="file-abc123", + endpoint="/v1/chat/completions", + completion_window="24h" + ) + node: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const batch = await openai.batches.create({ + input_file_id: "file-abc123", + endpoint: "/v1/chat/completions", + completion_window: "24h" + }); + + console.log(batch); + } + + main(); + response: | + { + "id": "batch_abc123", + "object": "batch", + "endpoint": "/v1/chat/completions", + "errors": null, + "input_file_id": "file-abc123", + "completion_window": "24h", + "status": "validating", + "output_file_id": null, + "error_file_id": null, + "created_at": 1711471533, + "in_progress_at": null, + "expires_at": null, + "finalizing_at": null, + "completed_at": null, + "failed_at": null, + "expired_at": null, + "cancelling_at": null, + "cancelled_at": null, + "request_counts": { + "total": 0, + "completed": 0, + "failed": 0 + }, + "metadata": { + "customer_id": "user_123456789", + "batch_description": "Nightly eval job", + } + } + get: + operationId: listBatches + tags: + - Batch + summary: List your organization's batches. + parameters: + - in: query + name: after + required: false + schema: + type: string + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + responses: + '200': + description: Batch listed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ListBatchesResponse' + x-oaiMeta: + name: List batch + group: batch + returns: A list of paginated [Batch](/docs/api-reference/batch/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/batches?limit=2 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" + python: | + from openai import OpenAI + client = OpenAI() + + client.batches.list() + node: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const list = await openai.batches.list(); + + for await (const batch of list) { + console.log(batch); + } + } + + main(); + response: | + { + "object": "list", + "data": [ + { + "id": "batch_abc123", + "object": "batch", + "endpoint": "/v1/chat/completions", + "errors": null, + "input_file_id": "file-abc123", + "completion_window": "24h", + "status": "completed", + "output_file_id": "file-cvaTdG", + "error_file_id": "file-HOWS94", + "created_at": 1711471533, + "in_progress_at": 1711471538, + "expires_at": 1711557933, + "finalizing_at": 1711493133, + "completed_at": 1711493163, + "failed_at": null, + "expired_at": null, + "cancelling_at": null, + "cancelled_at": null, + "request_counts": { + "total": 100, + "completed": 95, + "failed": 5 + }, + "metadata": { + "customer_id": "user_123456789", + "batch_description": "Nightly job", + } + }, + { ... }, + ], + "first_id": "batch_abc123", + "last_id": "batch_abc456", + "has_more": true + } + /batches/{batch_id}: + get: + operationId: retrieveBatch + tags: + - Batch + summary: Retrieves a batch. + parameters: + - in: path + name: batch_id + required: true + schema: + type: string + description: The ID of the batch to retrieve. + responses: + '200': + description: Batch retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Batch' + x-oaiMeta: + name: Retrieve batch + group: batch + returns: The [Batch](/docs/api-reference/batch/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/batches/batch_abc123 \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + python: | + from openai import OpenAI + client = OpenAI() + + client.batches.retrieve("batch_abc123") + node: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const batch = await openai.batches.retrieve("batch_abc123"); + + console.log(batch); + } + + main(); + response: | + { + "id": "batch_abc123", + "object": "batch", + "endpoint": "/v1/completions", + "errors": null, + "input_file_id": "file-abc123", + "completion_window": "24h", + "status": "completed", + "output_file_id": "file-cvaTdG", + "error_file_id": "file-HOWS94", + "created_at": 1711471533, + "in_progress_at": 1711471538, + "expires_at": 1711557933, + "finalizing_at": 1711493133, + "completed_at": 1711493163, + "failed_at": null, + "expired_at": null, + "cancelling_at": null, + "cancelled_at": null, + "request_counts": { + "total": 100, + "completed": 95, + "failed": 5 + }, + "metadata": { + "customer_id": "user_123456789", + "batch_description": "Nightly eval job", + } + } + /batches/{batch_id}/cancel: + post: + operationId: cancelBatch + tags: + - Batch + summary: Cancels an in-progress batch. The batch will be in status `cancelling` for up to 10 minutes, before changing to `cancelled`, where it will have partial results (if any) available in the output file. + parameters: + - in: path + name: batch_id + required: true + schema: + type: string + description: The ID of the batch to cancel. + responses: + '200': + description: Batch is cancelling. Returns the cancelling batch's details. + content: + application/json: + schema: + $ref: '#/components/schemas/Batch' + x-oaiMeta: + name: Cancel batch + group: batch + returns: The [Batch](/docs/api-reference/batch/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/batches/batch_abc123/cancel \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -X POST + python: | + from openai import OpenAI + client = OpenAI() + + client.batches.cancel("batch_abc123") + node: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + async function main() { + const batch = await openai.batches.cancel("batch_abc123"); + + console.log(batch); + } + + main(); + response: | + { + "id": "batch_abc123", + "object": "batch", + "endpoint": "/v1/chat/completions", + "errors": null, + "input_file_id": "file-abc123", + "completion_window": "24h", + "status": "cancelling", + "output_file_id": null, + "error_file_id": null, + "created_at": 1711471533, + "in_progress_at": 1711471538, + "expires_at": 1711557933, + "finalizing_at": null, + "completed_at": null, + "failed_at": null, + "expired_at": null, + "cancelling_at": 1711475133, + "cancelled_at": null, + "request_counts": { + "total": 100, + "completed": 23, + "failed": 1 + }, + "metadata": { + "customer_id": "user_123456789", + "batch_description": "Nightly eval job", + } + } + /organization/audit_logs: + get: + summary: List user actions and configuration changes within this organization. + operationId: list-audit-logs + tags: + - Audit Logs + parameters: + - name: effective_at + in: query + description: Return only events whose `effective_at` (Unix seconds) is in this range. + required: false + schema: + type: object + properties: + gt: + type: integer + description: Return only events whose `effective_at` (Unix seconds) is greater than this value. + gte: + type: integer + description: Return only events whose `effective_at` (Unix seconds) is greater than or equal to this value. + lt: + type: integer + description: Return only events whose `effective_at` (Unix seconds) is less than this value. + lte: + type: integer + description: Return only events whose `effective_at` (Unix seconds) is less than or equal to this value. + - name: project_ids[] + in: query + description: Return only events for these projects. + required: false + schema: + type: array + items: + type: string + - name: event_types[] + in: query + description: Return only events with a `type` in one of these values. For example, `project.created`. For all options, see the documentation for the [audit log object](/docs/api-reference/audit-logs/object). + required: false + schema: + type: array + items: + $ref: '#/components/schemas/AuditLogEventType' + - name: actor_ids[] + in: query + description: Return only events performed by these actors. Can be a user ID, a service account ID, or an api key tracking ID. + required: false + schema: + type: array + items: + type: string + - name: actor_emails[] + in: query + description: Return only events performed by users with these emails. + required: false + schema: + type: array + items: + type: string + - name: resource_ids[] + in: query + description: Return only events performed on these targets. For example, a project ID updated. + required: false + schema: + type: array + items: + type: string + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + schema: + type: string + - name: before + in: query + description: | + A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + schema: + type: string + responses: + '200': + description: Audit logs listed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ListAuditLogsResponse' + x-oaiMeta: + name: List audit logs + group: audit-logs + returns: A list of paginated [Audit Log](/docs/api-reference/audit-logs/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/audit_logs \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + response: | + { + "object": "list", + "data": [ + { + "id": "audit_log-xxx_yyyymmdd", + "type": "project.archived", + "effective_at": 1722461446, + "actor": { + "type": "api_key", + "api_key": { + "type": "user", + "user": { + "id": "user-xxx", + "email": "user@example.com" + } + } + }, + "project.archived": { + "id": "proj_abc" + }, + }, + { + "id": "audit_log-yyy__20240101", + "type": "api_key.updated", + "effective_at": 1720804190, + "actor": { + "type": "session", + "session": { + "user": { + "id": "user-xxx", + "email": "user@example.com" + }, + "ip_address": "127.0.0.1", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + } + }, + "api_key.updated": { + "id": "key_xxxx", + "data": { + "scopes": ["resource_2.operation_2"] + } + }, + } + ], + "first_id": "audit_log-xxx__20240101", + "last_id": "audit_log_yyy__20240101", + "has_more": true + } + /organization/invites: + get: + summary: Returns a list of invites in the organization. + operationId: list-invites + tags: + - Invites + parameters: + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + required: false + schema: + type: string + responses: + '200': + description: Invites listed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/InviteListResponse' + x-oaiMeta: + name: List invites + group: administration + returns: A list of [Invite](/docs/api-reference/invite/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/invites?after=invite-abc&limit=20 \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "list", + "data": [ + { + "object": "organization.invite", + "id": "invite-abc", + "email": "user@example.com", + "role": "owner", + "status": "accepted", + "invited_at": 1711471533, + "expires_at": 1711471533, + "accepted_at": 1711471533 + } + ], + "first_id": "invite-abc", + "last_id": "invite-abc", + "has_more": false + } + post: + summary: Create an invite for a user to the organization. The invite must be accepted by the user before they have access to the organization. + operationId: inviteUser + tags: + - Invites + requestBody: + description: The invite request payload. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InviteRequest' + responses: + '200': + description: User invited successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Invite' + x-oaiMeta: + name: Create invite + group: administration + returns: The created [Invite](/docs/api-reference/invite/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/invites \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "user@example.com", + "role": "owner" + }' + response: + content: | + { + "object": "organization.invite", + "id": "invite-abc", + "email": "user@example.com", + "role": "owner", + "invited_at": 1711471533, + "expires_at": 1711471533, + "accepted_at": null + } + /organization/invites/{invite_id}: + get: + summary: Retrieves an invite. + operationId: retrieve-invite + tags: + - Invites + parameters: + - in: path + name: invite_id + required: true + schema: + type: string + description: The ID of the invite to retrieve. + responses: + '200': + description: Invite retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Invite' + x-oaiMeta: + name: Retrieve invite + group: administration + returns: The [Invite](/docs/api-reference/invite/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/invites/invite-abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.invite", + "id": "invite-abc", + "email": "user@example.com", + "role": "owner", + "status": "accepted", + "invited_at": 1711471533, + "expires_at": 1711471533, + "accepted_at": 1711471533 + } + delete: + summary: Delete an invite. If the invite has already been accepted, it cannot be deleted. + operationId: delete-invite + tags: + - Invites + parameters: + - in: path + name: invite_id + required: true + schema: + type: string + description: The ID of the invite to delete. + responses: + '200': + description: Invite deleted successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/InviteDeleteResponse' + x-oaiMeta: + name: Delete invite + group: administration + returns: Confirmation that the invite has been deleted + examples: + request: + curl: | + curl -X DELETE https://api.openai.com/v1/organization/invites/invite-abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.invite.deleted", + "id": "invite-abc", + "deleted": true + } + /organization/users: + get: + summary: Lists all of the users in the organization. + operationId: list-users + tags: + - Users + parameters: + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + required: false + schema: + type: string + responses: + '200': + description: Users listed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/UserListResponse' + x-oaiMeta: + name: List users + group: administration + returns: A list of [User](/docs/api-reference/users/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/users?after=user_abc&limit=20 \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "list", + "data": [ + { + "object": "organization.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + ], + "first_id": "user-abc", + "last_id": "user-xyz", + "has_more": false + } + /organization/users/{user_id}: + get: + summary: Retrieves a user by their identifier. + operationId: retrieve-user + tags: + - Users + parameters: + - name: user_id + in: path + description: The ID of the user. + required: true + schema: + type: string + responses: + '200': + description: User retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/User' + x-oaiMeta: + name: Retrieve user + group: administration + returns: The [User](/docs/api-reference/users/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/users/user_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + post: + summary: Modifies a user's role in the organization. + operationId: modify-user + tags: + - Users + parameters: + - name: user_id + in: path + description: The ID of the user. + required: true + schema: + type: string + requestBody: + description: The new user role to modify. This must be one of `owner` or `member`. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserRoleUpdateRequest' + responses: + '200': + description: User role updated successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/User' + x-oaiMeta: + name: Modify user + group: administration + returns: The updated [User](/docs/api-reference/users/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/users/user_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "role": "owner" + }' + response: + content: | + { + "object": "organization.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + delete: + summary: Deletes a user from the organization. + operationId: delete-user + tags: + - Users + parameters: + - name: user_id + in: path + description: The ID of the user. + required: true + schema: + type: string + responses: + '200': + description: User deleted successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/UserDeleteResponse' + x-oaiMeta: + name: Delete user + group: administration + returns: Confirmation of the deleted user + examples: + request: + curl: | + curl -X DELETE https://api.openai.com/v1/organization/users/user_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.user.deleted", + "id": "user_abc", + "deleted": true + } + /organization/projects: + get: + summary: Returns a list of projects. + operationId: list-projects + tags: + - Projects + parameters: + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + required: false + schema: + type: string + - name: include_archived + in: query + schema: + type: boolean + default: false + description: If `true` returns all projects including those that have been `archived`. Archived projects are not included by default. + responses: + '200': + description: Projects listed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectListResponse' + x-oaiMeta: + name: List projects + group: administration + returns: A list of [Project](/docs/api-reference/projects/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/projects?after=proj_abc&limit=20&include_archived=false \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "list", + "data": [ + { + "id": "proj_abc", + "object": "organization.project", + "name": "Project example", + "created_at": 1711471533, + "archived_at": null, + "status": "active" + } + ], + "first_id": "proj-abc", + "last_id": "proj-xyz", + "has_more": false + } + post: + summary: Create a new project in the organization. Projects can be created and archived, but cannot be deleted. + operationId: create-project + tags: + - Projects + requestBody: + description: The project create request payload. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectCreateRequest' + responses: + '200': + description: Project created successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + x-oaiMeta: + name: Create project + group: administration + returns: The created [Project](/docs/api-reference/projects/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/projects \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Project ABC" + }' + response: + content: | + { + "id": "proj_abc", + "object": "organization.project", + "name": "Project ABC", + "created_at": 1711471533, + "archived_at": null, + "status": "active" + } + /organization/projects/{project_id}: + get: + summary: Retrieves a project. + operationId: retrieve-project + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + responses: + '200': + description: Project retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + x-oaiMeta: + name: Retrieve project + group: administration + description: Retrieve a project. + returns: The [Project](/docs/api-reference/projects/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/projects/proj_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "id": "proj_abc", + "object": "organization.project", + "name": "Project example", + "created_at": 1711471533, + "archived_at": null, + "status": "active" + } + post: + summary: Modifies a project in the organization. + operationId: modify-project + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + requestBody: + description: The project update request payload. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUpdateRequest' + responses: + '200': + description: Project updated successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '400': + description: Error response when updating the default project. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-oaiMeta: + name: Modify project + group: administration + returns: The updated [Project](/docs/api-reference/projects/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/projects/proj_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Project DEF" + }' + /organization/projects/{project_id}/archive: + post: + summary: Archives a project in the organization. Archived projects cannot be used or updated. + operationId: archive-project + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + responses: + '200': + description: Project archived successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + x-oaiMeta: + name: Archive project + group: administration + returns: The archived [Project](/docs/api-reference/projects/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/archive \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "id": "proj_abc", + "object": "organization.project", + "name": "Project DEF", + "created_at": 1711471533, + "archived_at": 1711471533, + "status": "archived" + } + /organization/projects/{project_id}/users: + get: + summary: Returns a list of users in the project. + operationId: list-project-users + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + required: false + schema: + type: string + responses: + '200': + description: Project users listed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUserListResponse' + '400': + description: Error response when project is archived. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-oaiMeta: + name: List project users + group: administration + returns: A list of [ProjectUser](/docs/api-reference/project-users/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/projects/proj_abc/users?after=user_abc&limit=20 \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "list", + "data": [ + { + "object": "organization.project.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + ], + "first_id": "user-abc", + "last_id": "user-xyz", + "has_more": false + } + error_response: + content: | + { + "code": 400, + "message": "Project {name} is archived" + } + post: + summary: Adds a user to the project. Users must already be members of the organization to be added to a project. + operationId: create-project-user + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + tags: + - Projects + requestBody: + description: The project user create request payload. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUserCreateRequest' + responses: + '200': + description: User added to project successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUser' + '400': + description: Error response for various conditions. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-oaiMeta: + name: Create project user + group: administration + returns: The created [ProjectUser](/docs/api-reference/project-users/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/users \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "user_abc", + "role": "member" + }' + response: + content: | + { + "object": "organization.project.user", + "id": "user_abc", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + error_response: + content: | + { + "code": 400, + "message": "Project {name} is archived" + } + /organization/projects/{project_id}/users/{user_id}: + get: + summary: Retrieves a user in the project. + operationId: retrieve-project-user + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: user_id + in: path + description: The ID of the user. + required: true + schema: + type: string + responses: + '200': + description: Project user retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUser' + x-oaiMeta: + name: Retrieve project user + group: administration + returns: The [ProjectUser](/docs/api-reference/project-users/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/projects/proj_abc/users/user_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.project.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + post: + summary: Modifies a user's role in the project. + operationId: modify-project-user + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: user_id + in: path + description: The ID of the user. + required: true + schema: + type: string + requestBody: + description: The project user update request payload. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUserUpdateRequest' + responses: + '200': + description: Project user's role updated successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUser' + '400': + description: Error response for various conditions. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-oaiMeta: + name: Modify project user + group: administration + returns: The updated [ProjectUser](/docs/api-reference/project-users/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/users/user_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "role": "owner" + }' + response: + content: | + { + "object": "organization.project.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + delete: + summary: Deletes a user from the project. + operationId: delete-project-user + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: user_id + in: path + description: The ID of the user. + required: true + schema: + type: string + responses: + '200': + description: Project user deleted successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUserDeleteResponse' + '400': + description: Error response for various conditions. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-oaiMeta: + name: Delete project user + group: administration + returns: Confirmation that project has been deleted or an error in case of an archived project, which has no users + examples: + request: + curl: | + curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc/users/user_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.project.user.deleted", + "id": "user_abc", + "deleted": true + } + /organization/projects/{project_id}/service_accounts: + get: + summary: Returns a list of service accounts in the project. + operationId: list-project-service-accounts + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + required: false + schema: + type: string + responses: + '200': + description: Project service accounts listed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectServiceAccountListResponse' + '400': + description: Error response when project is archived. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-oaiMeta: + name: List project service accounts + group: administration + returns: A list of [ProjectServiceAccount](/docs/api-reference/project-service-accounts/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/projects/proj_abc/service_accounts?after=custom_id&limit=20 \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "list", + "data": [ + { + "object": "organization.project.service_account", + "id": "svc_acct_abc", + "name": "Service Account", + "role": "owner", + "created_at": 1711471533 + } + ], + "first_id": "svc_acct_abc", + "last_id": "svc_acct_xyz", + "has_more": false + } + post: + summary: Creates a new service account in the project. This also returns an unredacted API key for the service account. + operationId: create-project-service-account + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + requestBody: + description: The project service account create request payload. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectServiceAccountCreateRequest' + responses: + '200': + description: Project service account created successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectServiceAccountCreateResponse' + '400': + description: Error response when project is archived. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-oaiMeta: + name: Create project service account + group: administration + returns: The created [ProjectServiceAccount](/docs/api-reference/project-service-accounts/object) object. + examples: + request: + curl: | + curl -X POST https://api.openai.com/v1/organization/projects/proj_abc/service_accounts \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Production App" + }' + response: + content: | + { + "object": "organization.project.service_account", + "id": "svc_acct_abc", + "name": "Production App", + "role": "member", + "created_at": 1711471533, + "api_key": { + "object": "organization.project.service_account.api_key", + "value": "sk-abcdefghijklmnop123", + "name": "Secret Key", + "created_at": 1711471533, + "id": "key_abc" + } + } + /organization/projects/{project_id}/service_accounts/{service_account_id}: + get: + summary: Retrieves a service account in the project. + operationId: retrieve-project-service-account + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: service_account_id + in: path + description: The ID of the service account. + required: true + schema: + type: string + responses: + '200': + description: Project service account retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectServiceAccount' + x-oaiMeta: + name: Retrieve project service account + group: administration + returns: The [ProjectServiceAccount](/docs/api-reference/project-service-accounts/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/projects/proj_abc/service_accounts/svc_acct_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.project.service_account", + "id": "svc_acct_abc", + "name": "Service Account", + "role": "owner", + "created_at": 1711471533 + } + delete: + summary: Deletes a service account from the project. + operationId: delete-project-service-account + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: service_account_id + in: path + description: The ID of the service account. + required: true + schema: + type: string + responses: + '200': + description: Project service account deleted successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectServiceAccountDeleteResponse' + x-oaiMeta: + name: Delete project service account + group: administration + returns: Confirmation of service account being deleted, or an error in case of an archived project, which has no service accounts + examples: + request: + curl: | + curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc/service_accounts/svc_acct_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.project.service_account.deleted", + "id": "svc_acct_abc", + "deleted": true + } + /organization/projects/{project_id}/api_keys: + get: + summary: Returns a list of API keys in the project. + operationId: list-project-api-keys + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: limit + in: query + description: | + A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + required: false + schema: + type: integer + default: 20 + - name: after + in: query + description: | + A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list. + required: false + schema: + type: string + responses: + '200': + description: Project API keys listed successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectApiKeyListResponse' + x-oaiMeta: + name: List project API keys + group: administration + returns: A list of [ProjectApiKey](/docs/api-reference/project-api-keys/object) objects. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/projects/proj_abc/api_keys?after=key_abc&limit=20 \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "list", + "data": [ + { + "object": "organization.project.api_key", + "redacted_value": "sk-abc...def", + "name": "My API Key", + "created_at": 1711471533, + "id": "key_abc", + "owner": { + "type": "user", + "user": { + "object": "organization.project.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + } + } + ], + "first_id": "key_abc", + "last_id": "key_xyz", + "has_more": false + } + error_response: + content: | + { + "code": 400, + "message": "Project {name} is archived" + } + /organization/projects/{project_id}/api_keys/{key_id}: + get: + summary: Retrieves an API key in the project. + operationId: retrieve-project-api-key + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: key_id + in: path + description: The ID of the API key. + required: true + schema: + type: string + responses: + '200': + description: Project API key retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectApiKey' + x-oaiMeta: + name: Retrieve project API key + group: administration + returns: The [ProjectApiKey](/docs/api-reference/project-api-keys/object) object matching the specified ID. + examples: + request: + curl: | + curl https://api.openai.com/v1/organization/projects/proj_abc/api_keys/key_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.project.api_key", + "redacted_value": "sk-abc...def", + "name": "My API Key", + "created_at": 1711471533, + "id": "key_abc", + "owner": { + "type": "user", + "user": { + "object": "organization.project.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + } + } + delete: + summary: Deletes an API key from the project. + operationId: delete-project-api-key + tags: + - Projects + parameters: + - name: project_id + in: path + description: The ID of the project. + required: true + schema: + type: string + - name: key_id + in: path + description: The ID of the API key. + required: true + schema: + type: string + responses: + '200': + description: Project API key deleted successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectApiKeyDeleteResponse' + '400': + description: Error response for various conditions. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + x-oaiMeta: + name: Delete project API key + group: administration + returns: Confirmation of the key's deletion or an error if the key belonged to a service account + examples: + request: + curl: | + curl -X DELETE https://api.openai.com/v1/organization/projects/proj_abc/api_keys/key_abc \ + -H "Authorization: Bearer $OPENAI_ADMIN_KEY" \ + -H "Content-Type: application/json" + response: + content: | + { + "object": "organization.project.api_key.deleted", + "id": "key_abc", + "deleted": true + } + error_response: + content: | + { + "code": 400, + "message": "API keys cannot be deleted for service accounts, please delete the service account" + } + /responses: + post: + operationId: createResponse + tags: + - Responses + - text-generation + summary: > + Creates a model response. Provide [text](/docs/guides/text) or + + [image](/docs/guides/images) inputs to generate + [text](/docs/guides/text) + + or [JSON](/docs/guides/structured-outputs) outputs. Have the model call + + your own [custom code](/docs/guides/function-calling) or use built-in + + [tools](/docs/guides/tools) like [web + search](/docs/guides/tools-web-search) + + or [file search](/docs/guides/tools-file-search) to use your own data + + as input for the model's response. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateResponse" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Response" + text/event-stream: + schema: + $ref: "#/components/schemas/ResponseStreamEvent" + x-oaiMeta: + name: Create a model response + group: responses + returns: | + Returns a [Response](/docs/api-reference/responses/object) object. + path: create + examples: + - title: Text input + request: + curl: > + curl https://api.openai.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-4.1", + "input": "Tell me a three sentence bedtime story about a unicorn." + }' + javascript: > + import OpenAI from "openai"; + + + const openai = new OpenAI(); + + + const response = await openai.responses.create({ + model: "gpt-4.1", + input: "Tell me a three sentence bedtime story about a unicorn." + }); + + + console.log(response); + python: > + from openai import OpenAI + + + client = OpenAI() + + + response = client.responses.create( + model="gpt-4.1", + input="Tell me a three sentence bedtime story about a unicorn." + ) + + + print(response) + csharp: > + using System; + + using OpenAI.Responses; + + + OpenAIResponseClient client = new( + model: "gpt-4.1", + apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY") + ); + + + OpenAIResponse response = client.CreateResponse("Tell me a three + sentence bedtime story about a unicorn."); + + + Console.WriteLine(response.GetOutputText()); + response: > + { + "id": "resp_67ccd2bed1ec8190b14f964abc0542670bb6a6b452d3795b", + "object": "response", + "created_at": 1741476542, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "type": "message", + "id": "msg_67ccd2bf17f0819081ff3bb2cf6508e60bb6a6b452d3795b", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "In a peaceful grove beneath a silver moon, a unicorn named Lumina discovered a hidden pool that reflected the stars. As she dipped her horn into the water, the pool began to shimmer, revealing a pathway to a magical realm of endless night skies. Filled with wonder, Lumina whispered a wish for all who dream to find their own hidden magic, and as she glanced back, her hoofprints sparkled like stardust.", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 36, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 87, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 123 + }, + "user": null, + "metadata": {} + } + - title: Image input + request: + curl: > + curl https://api.openai.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-4.1", + "input": [ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "what is in this image?"}, + { + "type": "input_image", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + } + ] + } + ] + }' + javascript: > + import OpenAI from "openai"; + + + const openai = new OpenAI(); + + + const response = await openai.responses.create({ + model: "gpt-4.1", + input: [ + { + role: "user", + content: [ + { type: "input_text", text: "what is in this image?" }, + { + type: "input_image", + image_url: + "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + }, + ], + }, + ], + }); + + + console.log(response); + python: > + from openai import OpenAI + + + client = OpenAI() + + + response = client.responses.create( + model="gpt-4.1", + input=[ + { + "role": "user", + "content": [ + { "type": "input_text", "text": "what is in this image?" }, + { + "type": "input_image", + "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" + } + ] + } + ] + ) + + + print(response) + csharp: > + using System; + + using System.Collections.Generic; + + + using OpenAI.Responses; + + + OpenAIResponseClient client = new( + model: "gpt-4.1", + apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY") + ); + + + List inputItems = + + [ + ResponseItem.CreateUserMessageItem( + [ + ResponseContentPart.CreateInputTextPart("What is in this image?"), + ResponseContentPart.CreateInputImagePart(new Uri("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg")) + ] + ) + ]; + + + OpenAIResponse response = client.CreateResponse(inputItems); + + + Console.WriteLine(response.GetOutputText()); + response: > + { + "id": "resp_67ccd3a9da748190baa7f1570fe91ac604becb25c45c1d41", + "object": "response", + "created_at": 1741476777, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "type": "message", + "id": "msg_67ccd3acc8d48190a77525dc6de64b4104becb25c45c1d41", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "The image depicts a scenic landscape with a wooden boardwalk or pathway leading through lush, green grass under a blue sky with some clouds. The setting suggests a peaceful natural area, possibly a park or nature reserve. There are trees and shrubs in the background.", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 328, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 52, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 380 + }, + "user": null, + "metadata": {} + } + - title: Web search + request: + curl: | + curl https://api.openai.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-4.1", + "tools": [{ "type": "web_search_preview" }], + "input": "What was a positive news story from today?" + }' + javascript: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + const response = await openai.responses.create({ + model: "gpt-4.1", + tools: [{ type: "web_search_preview" }], + input: "What was a positive news story from today?", + }); + + console.log(response); + python: | + from openai import OpenAI + + client = OpenAI() + + response = client.responses.create( + model="gpt-4.1", + tools=[{ "type": "web_search_preview" }], + input="What was a positive news story from today?", + ) + + print(response) + csharp: > + using System; + + + using OpenAI.Responses; + + + OpenAIResponseClient client = new( + model: "gpt-4.1", + apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY") + ); + + + string userInputText = "What was a positive news story from + today?"; + + + ResponseCreationOptions options = new() + + { + Tools = + { + ResponseTool.CreateWebSearchTool() + }, + }; + + + OpenAIResponse response = client.CreateResponse(userInputText, + options); + + + Console.WriteLine(response.GetOutputText()); + response: > + { + "id": "resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c", + "object": "response", + "created_at": 1741484430, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "type": "web_search_call", + "id": "ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c", + "status": "completed" + }, + { + "type": "message", + "id": "msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "As of today, March 9, 2025, one notable positive news story...", + "annotations": [ + { + "type": "url_citation", + "start_index": 442, + "end_index": 557, + "url": "https://.../?utm_source=chatgpt.com", + "title": "..." + }, + { + "type": "url_citation", + "start_index": 962, + "end_index": 1077, + "url": "https://.../?utm_source=chatgpt.com", + "title": "..." + }, + { + "type": "url_citation", + "start_index": 1336, + "end_index": 1451, + "url": "https://.../?utm_source=chatgpt.com", + "title": "..." + } + ] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "web_search_preview", + "domains": [], + "search_context_size": "medium", + "user_location": { + "type": "approximate", + "city": null, + "country": "US", + "region": null, + "timezone": null + } + } + ], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 328, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 356, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 684 + }, + "user": null, + "metadata": {} + } + - title: File search + request: + curl: > + curl https://api.openai.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-4.1", + "tools": [{ + "type": "file_search", + "vector_store_ids": ["vs_1234567890"], + "max_num_results": 20 + }], + "input": "What are the attributes of an ancient brown dragon?" + }' + javascript: > + import OpenAI from "openai"; + + + const openai = new OpenAI(); + + + const response = await openai.responses.create({ + model: "gpt-4.1", + tools: [{ + type: "file_search", + vector_store_ids: ["vs_1234567890"], + max_num_results: 20 + }], + input: "What are the attributes of an ancient brown dragon?", + }); + + + console.log(response); + python: | + from openai import OpenAI + + client = OpenAI() + + response = client.responses.create( + model="gpt-4.1", + tools=[{ + "type": "file_search", + "vector_store_ids": ["vs_1234567890"], + "max_num_results": 20 + }], + input="What are the attributes of an ancient brown dragon?", + ) + + print(response) + csharp: > + using System; + + + using OpenAI.Responses; + + + OpenAIResponseClient client = new( + model: "gpt-4.1", + apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY") + ); + + + string userInputText = "What are the attributes of an ancient + brown dragon?"; + + + ResponseCreationOptions options = new() + + { + Tools = + { + ResponseTool.CreateFileSearchTool( + vectorStoreIds: ["vs_1234567890"], + maxResultCount: 20 + ) + }, + }; + + + OpenAIResponse response = client.CreateResponse(userInputText, + options); + + + Console.WriteLine(response.GetOutputText()); + response: > + { + "id": "resp_67ccf4c55fc48190b71bd0463ad3306d09504fb6872380d7", + "object": "response", + "created_at": 1741485253, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "type": "file_search_call", + "id": "fs_67ccf4c63cd08190887ef6464ba5681609504fb6872380d7", + "status": "completed", + "queries": [ + "attributes of an ancient brown dragon" + ], + "results": null + }, + { + "type": "message", + "id": "msg_67ccf4c93e5c81909d595b369351a9d309504fb6872380d7", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "The attributes of an ancient brown dragon include...", + "annotations": [ + { + "type": "file_citation", + "index": 320, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 576, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 815, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 815, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 1030, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 1030, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 1156, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + }, + { + "type": "file_citation", + "index": 1225, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + } + ] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "file_search", + "filters": null, + "max_num_results": 20, + "ranking_options": { + "ranker": "auto", + "score_threshold": 0.0 + }, + "vector_store_ids": [ + "vs_1234567890" + ] + } + ], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 18307, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 348, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 18655 + }, + "user": null, + "metadata": {} + } + - title: Streaming + request: + curl: | + curl https://api.openai.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-4.1", + "instructions": "You are a helpful assistant.", + "input": "Hello!", + "stream": true + }' + python: | + from openai import OpenAI + + client = OpenAI() + + response = client.responses.create( + model="gpt-4.1", + instructions="You are a helpful assistant.", + input="Hello!", + stream=True + ) + + for event in response: + print(event) + javascript: | + import OpenAI from "openai"; + + const openai = new OpenAI(); + + const response = await openai.responses.create({ + model: "gpt-4.1", + instructions: "You are a helpful assistant.", + input: "Hello!", + stream: true, + }); + + for await (const event of response) { + console.log(event); + } + csharp: > + using System; + + using System.ClientModel; + + using System.Threading.Tasks; + + + using OpenAI.Responses; + + + OpenAIResponseClient client = new( + model: "gpt-4.1", + apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY") + ); + + + string userInputText = "Hello!"; + + + ResponseCreationOptions options = new() + + { + Instructions = "You are a helpful assistant.", + }; + + + AsyncCollectionResult responseUpdates = + client.CreateResponseStreamingAsync(userInputText, options); + + + await foreach (StreamingResponseUpdate responseUpdate in + responseUpdates) + + { + if (responseUpdate is StreamingResponseOutputTextDeltaUpdate outputTextDeltaUpdate) + { + Console.Write(outputTextDeltaUpdate.Delta); + } + } + response: | + event: response.created + data: {"type":"response.created","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + + event: response.output_item.added + data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"in_progress","role":"assistant","content":[]}} + + event: response.content_part.added + data: {"type":"response.content_part.added","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"Hi"} + + ... + + event: response.output_text.done + data: {"type":"response.output_text.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"text":"Hi there! How can I assist you today?"} + + event: response.content_part.done + data: {"type":"response.content_part.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}} + + event: response.output_item.done + data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}]}} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"completed","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":37,"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":48},"user":null,"metadata":{}}} + - title: Functions + request: + curl: > + curl https://api.openai.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-4.1", + "input": "What is the weather like in Boston today?", + "tools": [ + { + "type": "function", + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location", "unit"] + } + } + ], + "tool_choice": "auto" + }' + python: > + from openai import OpenAI + + + client = OpenAI() + + + tools = [ + { + "type": "function", + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location", "unit"], + } + } + ] + + + response = client.responses.create( + model="gpt-4.1", + tools=tools, + input="What is the weather like in Boston today?", + tool_choice="auto" + ) + + + print(response) + javascript: > + import OpenAI from "openai"; + + + const openai = new OpenAI(); + + + const tools = [ + { + type: "function", + name: "get_current_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA", + }, + unit: { type: "string", enum: ["celsius", "fahrenheit"] }, + }, + required: ["location", "unit"], + }, + }, + ]; + + + const response = await openai.responses.create({ + model: "gpt-4.1", + tools: tools, + input: "What is the weather like in Boston today?", + tool_choice: "auto", + }); + + + console.log(response); + csharp: > + using System; + + using OpenAI.Responses; + + + OpenAIResponseClient client = new( + model: "gpt-4.1", + apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY") + ); + + + ResponseTool getCurrentWeatherFunctionTool = + ResponseTool.CreateFunctionTool( + functionName: "get_current_weather", + functionDescription: "Get the current weather in a given location", + functionParameters: BinaryData.FromString(""" + { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} + }, + "required": ["location", "unit"] + } + """ + ) + ); + + + string userInputText = "What is the weather like in Boston + today?"; + + + ResponseCreationOptions options = new() + + { + Tools = + { + getCurrentWeatherFunctionTool + }, + ToolChoice = ResponseToolChoice.CreateAutoChoice(), + }; + + + OpenAIResponse response = client.CreateResponse(userInputText, + options); + response: > + { + "id": "resp_67ca09c5efe0819096d0511c92b8c890096610f474011cc0", + "object": "response", + "created_at": 1741294021, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "type": "function_call", + "id": "fc_67ca09c6bedc8190a7abfec07b1a1332096610f474011cc0", + "call_id": "call_unLAR8MvFNptuiZK6K6HCy5k", + "name": "get_current_weather", + "arguments": "{\"location\":\"Boston, MA\",\"unit\":\"celsius\"}", + "status": "completed" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get the current weather in a given location", + "name": "get_current_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": [ + "celsius", + "fahrenheit" + ] + } + }, + "required": [ + "location", + "unit" + ] + }, + "strict": true + } + ], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 291, + "output_tokens": 23, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 314 + }, + "user": null, + "metadata": {} + } + - title: Reasoning + request: + curl: | + curl https://api.openai.com/v1/responses \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "o3-mini", + "input": "How much wood would a woodchuck chuck?", + "reasoning": { + "effort": "high" + } + }' + javascript: | + import OpenAI from "openai"; + const openai = new OpenAI(); + + const response = await openai.responses.create({ + model: "o3-mini", + input: "How much wood would a woodchuck chuck?", + reasoning: { + effort: "high" + } + }); + + console.log(response); + python: | + from openai import OpenAI + client = OpenAI() + + response = client.responses.create( + model="o3-mini", + input="How much wood would a woodchuck chuck?", + reasoning={ + "effort": "high" + } + ) + + print(response) + csharp: > + using System; + + using OpenAI.Responses; + + + OpenAIResponseClient client = new( + model: "o3-mini", + apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY") + ); + + + string userInputText = "How much wood would a woodchuck chuck?"; + + + ResponseCreationOptions options = new() + + { + ReasoningOptions = new() + { + ReasoningEffortLevel = ResponseReasoningEffortLevel.High, + }, + }; + + + OpenAIResponse response = client.CreateResponse(userInputText, + options); + + + Console.WriteLine(response.GetOutputText()); + response: > + { + "id": "resp_67ccd7eca01881908ff0b5146584e408072912b2993db808", + "object": "response", + "created_at": 1741477868, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "o1-2024-12-17", + "output": [ + { + "type": "message", + "id": "msg_67ccd7f7b5848190a6f3e95d809f6b44072912b2993db808", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "The classic tongue twister...", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": "high", + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 81, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 1035, + "output_tokens_details": { + "reasoning_tokens": 832 + }, + "total_tokens": 1116 + }, + "user": null, + "metadata": {} + } +components: + securitySchemes: + ApiKeyAuth: + type: http + scheme: bearer + schemas: + Error: + type: object + properties: + code: + type: string + nullable: true + message: + type: string + nullable: false + param: + type: string + nullable: true + type: + type: string + nullable: false + required: + - type + - message + - param + - code + ErrorResponse: + type: object + properties: + error: + $ref: '#/components/schemas/Error' + required: + - error + ListModelsResponse: + type: object + properties: + object: + type: string + enum: + - list + data: + type: array + items: + $ref: '#/components/schemas/Model' + required: + - object + - data + DeleteModelResponse: + type: object + properties: + id: + type: string + deleted: + type: boolean + object: + type: string + required: + - id + - object + - deleted + ModelIdsShared: + example: gpt-4o + anyOf: + - type: string + - type: string + enum: + - gpt-4.1 + - gpt-4.1-mini + - gpt-4.1-nano + - gpt-4.1-2025-04-14 + - gpt-4.1-mini-2025-04-14 + - gpt-4.1-nano-2025-04-14 + - o4-mini + - o4-mini-2025-04-16 + - o3 + - o3-2025-04-16 + - o3-mini + - o3-mini-2025-01-31 + - o1 + - o1-2024-12-17 + - o1-preview + - o1-preview-2024-09-12 + - o1-mini + - o1-mini-2024-09-12 + - gpt-4o + - gpt-4o-2024-11-20 + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o-audio-preview + - gpt-4o-audio-preview-2024-10-01 + - gpt-4o-audio-preview-2024-12-17 + - gpt-4o-mini-audio-preview + - gpt-4o-mini-audio-preview-2024-12-17 + - gpt-4o-search-preview + - gpt-4o-mini-search-preview + - gpt-4o-search-preview-2025-03-11 + - gpt-4o-mini-search-preview-2025-03-11 + - chatgpt-4o-latest + - gpt-4o-mini + - gpt-4o-mini-2024-07-18 + - gpt-4-turbo + - gpt-4-turbo-2024-04-09 + - gpt-4-0125-preview + - gpt-4-turbo-preview + - gpt-4-1106-preview + - gpt-4-vision-preview + - gpt-4 + - gpt-4-0314 + - gpt-4-0613 + - gpt-4-32k + - gpt-4-32k-0314 + - gpt-4-32k-0613 + - gpt-3.5-turbo + - gpt-3.5-turbo-16k + - gpt-3.5-turbo-0301 + - gpt-3.5-turbo-0613 + - gpt-3.5-turbo-1106 + - gpt-3.5-turbo-0125 + - gpt-3.5-turbo-16k-0613 + ModelIdsResponses: + example: gpt-4o + anyOf: + - $ref: "#/components/schemas/ModelIdsShared" + - type: string + title: ResponsesOnlyModel + enum: + - o1-pro + - o1-pro-2025-03-19 + - computer-use-preview + - computer-use-preview-2025-03-11 + ResponseProperties: + type: object + properties: + previous_response_id: + type: string + description: | + The unique ID of the previous response to the model. Use this to + create multi-turn conversations. Learn more about + [conversation state](/docs/guides/conversation-state). + nullable: true + model: + description: > + Model ID used to generate the response, like `gpt-4o` or `o3`. + OpenAI + + offers a wide range of models with different capabilities, + performance + + characteristics, and price points. Refer to the [model + guide](/docs/models) + + to browse and compare available models. + $ref: "#/components/schemas/ModelIdsResponses" + reasoning: + $ref: "#/components/schemas/Reasoning" + nullable: true + max_output_tokens: + description: > + An upper bound for the number of tokens that can be generated for a + response, including visible output tokens and [reasoning + tokens](/docs/guides/reasoning). + type: integer + nullable: true + instructions: + type: string + description: > + Inserts a system (or developer) message as the first item in the + model's context. + + + When using along with `previous_response_id`, the instructions from + a previous + + response will not be carried over to the next response. This makes + it simple + + to swap out system (or developer) messages in new responses. + nullable: true + text: + type: object + description: > + Configuration options for a text response from the model. Can be + plain + + text or structured JSON data. Learn more: + + - [Text inputs and outputs](/docs/guides/text) + + - [Structured Outputs](/docs/guides/structured-outputs) + properties: + format: + $ref: "#/components/schemas/TextResponseFormatConfiguration" + tools: + type: array + description: > + An array of tools the model may call while generating a response. + You + + can specify which tool to use by setting the `tool_choice` + parameter. + + + The two categories of tools you can provide the model are: + + + - **Built-in tools**: Tools that are provided by OpenAI that extend + the + model's capabilities, like [web search](/docs/guides/tools-web-search) + or [file search](/docs/guides/tools-file-search). Learn more about + [built-in tools](/docs/guides/tools). + - **Function calls (custom tools)**: Functions that are defined by + you, + enabling the model to call your own code. Learn more about + [function calling](/docs/guides/function-calling). + items: + $ref: "#/components/schemas/Tool" + tool_choice: + description: > + How the model should select which tool (or tools) to use when + generating + + a response. See the `tools` parameter to see how to specify which + tools + + the model can call. + oneOf: + - $ref: "#/components/schemas/ToolChoiceOptions" + - $ref: "#/components/schemas/ToolChoiceTypes" + - $ref: "#/components/schemas/ToolChoiceFunction" + truncation: + type: string + description: > + The truncation strategy to use for the model response. + + - `auto`: If the context of this response and previous ones exceeds + the model's context window size, the model will truncate the + response to fit the context window by dropping input items in the + middle of the conversation. + - `disabled` (default): If a model response will exceed the context + window + size for a model, the request will fail with a 400 error. + enum: + - auto + - disabled + nullable: true + default: disabled + Reasoning: + type: object + description: | + **o-series models only** + + Configuration options for + [reasoning models](https://platform.openai.com/docs/guides/reasoning). + title: Reasoning + properties: + effort: + $ref: "#/components/schemas/ReasoningEffort" + summary: + type: string + description: > + A summary of the reasoning performed by the model. This can be + + useful for debugging and understanding the model's reasoning + process. + + One of `auto`, `concise`, or `detailed`. + enum: + - auto + - concise + - detailed + nullable: true + generate_summary: + type: string + deprecated: true + description: > + **Deprecated:** use `summary` instead. + + + A summary of the reasoning performed by the model. This can be + + useful for debugging and understanding the model's reasoning + process. + + One of `auto`, `concise`, or `detailed`. + enum: + - auto + - concise + - detailed + nullable: true + ReasoningEffort: + type: string + enum: + - low + - medium + - high + default: medium + nullable: true + description: | + **o-series models only** + + Constrains effort on reasoning for + [reasoning models](https://platform.openai.com/docs/guides/reasoning). + Currently supported values are `low`, `medium`, and `high`. Reducing + reasoning effort can result in faster responses and fewer tokens used + on reasoning in a response. + ToolChoiceFunction: + type: object + title: Function tool + description: | + Use this option to force the model to call a specific function. + properties: + type: + type: string + enum: + - function + description: For function calling, the type is always `function`. + x-stainless-const: true + name: + type: string + description: The name of the function to call. + required: + - type + - name + WebSearchPreviewTool: + properties: + type: + type: string + enum: + - web_search_preview + - web_search_preview_2025_03_11 + description: The type of the web search tool. One of `web_search_preview` or + `web_search_preview_2025_03_11`. + default: web_search_preview + x-stainless-const: true + user_location: + anyOf: + - $ref: "#/components/schemas/ApproximateLocation" + description: The user's location + search_context_size: + type: string + enum: + - low + - medium + - high + description: High level guidance for the amount of context window space to use + for the search. One of `low`, `medium`, or `high`. `medium` is the + default. + title: Web search preview + description: This tool searches the web for relevant results to use in a + response. Learn more about the [web search + tool](https://platform.openai.com/docs/guides/tools-web-search). + FileSearchTool: + properties: + type: + type: string + enum: + - file_search + description: The type of the file search tool. Always `file_search`. + default: file_search + x-stainless-const: true + vector_store_ids: + items: + type: string + type: array + description: The IDs of the vector stores to search. + max_num_results: + type: integer + description: The maximum number of results to return. This number should be + between 1 and 50 inclusive. + ranking_options: + $ref: "#/components/schemas/RankingOptions" + description: Ranking options for search. + filters: + anyOf: + - $ref: "#/components/schemas/Filters" + description: A filter to apply. + type: object + required: + - type + - vector_store_ids + title: File search + description: A tool that searches for relevant content from uploaded files. + Learn more about the [file search + tool](https://platform.openai.com/docs/guides/tools-file-search). + Tool: + oneOf: + - $ref: "#/components/schemas/FileSearchTool" + - $ref: "#/components/schemas/FunctionTool" + - $ref: "#/components/schemas/WebSearchPreviewTool" + - $ref: "#/components/schemas/ComputerUsePreviewTool" + discriminator: + propertyName: type + Filters: + anyOf: + - $ref: "#/components/schemas/ComparisonFilter" + - $ref: "#/components/schemas/CompoundFilter" + ApproximateLocation: + properties: + type: + type: string + enum: + - approximate + description: The type of location approximation. Always `approximate`. + default: approximate + x-stainless-const: true + country: + anyOf: + - type: string + description: The two-letter [ISO country + code](https://en.wikipedia.org/wiki/ISO_3166-1) of the user, + e.g. `US`. + region: + anyOf: + - type: string + description: Free text input for the region of the user, e.g. `California`. + city: + anyOf: + - type: string + description: Free text input for the city of the user, e.g. `San Francisco`. + timezone: + anyOf: + - type: string + description: The [IANA + timezone](https://timeapi.io/documentation/iana-timezones) of + the user, e.g. `America/Los_Angeles`. + type: object + FunctionTool: + properties: + type: + type: string + enum: + - function + description: The type of the function tool. Always `function`. + default: function + x-stainless-const: true + name: + type: string + description: The name of the function to call. + description: + anyOf: + - type: string + description: A description of the function. Used by the model to determine + whether or not to call the function. + parameters: + anyOf: + - additionalProperties: {} + type: object + description: A JSON schema object describing the parameters of the function. + strict: + anyOf: + - type: boolean + description: Whether to enforce strict parameter validation. Default `true`. + type: object + required: + - type + - name + - strict + - parameters + title: Function + description: Defines a function in your own code the model can choose to call. + Learn more about [function + calling](https://platform.openai.com/docs/guides/function-calling). + CompoundFilter: + type: object + additionalProperties: false + title: Compound Filter + description: Combine multiple filters using `and` or `or`. + properties: + type: + type: string + description: "Type of operation: `and` or `or`." + enum: + - and + - or + filters: + type: array + description: Array of filters to combine. Items can be `ComparisonFilter` or + `CompoundFilter`. + items: + oneOf: + - $ref: "#/components/schemas/ComparisonFilter" + required: + - type + - filters + x-oaiMeta: + name: CompoundFilter + ComputerUsePreviewTool: + properties: + type: + type: string + enum: + - computer_use_preview + description: The type of the computer use tool. Always `computer_use_preview`. + default: computer_use_preview + x-stainless-const: true + environment: + type: string + enum: + - windows + - mac + - linux + - ubuntu + - browser + description: The type of computer environment to control. + display_width: + type: integer + description: The width of the computer display. + display_height: + type: integer + description: The height of the computer display. + type: object + required: + - type + - environment + - display_width + - display_height + title: Computer use preview + description: A tool that controls a virtual computer. Learn more about the + [computer + tool](https://platform.openai.com/docs/guides/tools-computer-use). + RankingOptions: + properties: + ranker: + type: string + enum: + - auto + - default-2024-11-15 + description: The ranker to use for the file search. + score_threshold: + type: number + description: The score threshold for the file search, a number between 0 and 1. + Numbers closer to 1 will attempt to return only the most relevant + results, but may return fewer results. + type: object + ComparisonFilter: + type: object + additionalProperties: false + title: Comparison Filter + description: > + A filter used to compare a specified attribute key to a given value + using a defined comparison operation. + properties: + type: + type: string + default: eq + enum: + - eq + - ne + - gt + - gte + - lt + - lte + description: > + Specifies the comparison operator: `eq`, `ne`, `gt`, `gte`, `lt`, + `lte`. + + - `eq`: equals + + - `ne`: not equal + + - `gt`: greater than + + - `gte`: greater than or equal + + - `lt`: less than + + - `lte`: less than or equal + key: + type: string + description: The key to compare against the value. + value: + oneOf: + - type: string + - type: number + - type: boolean + description: The value to compare against the attribute key; supports string, + number, or boolean types. + required: + - type + - key + - value + x-oaiMeta: + name: ComparisonFilter + ToolChoiceTypes: + type: object + title: Hosted tool + description: > + Indicates that the model should use a built-in tool to generate a + response. + + [Learn more about built-in tools](/docs/guides/tools). + properties: + type: + type: string + description: | + The type of hosted tool the model should to use. Learn more about + [built-in tools](/docs/guides/tools). + + Allowed values are: + - `file_search` + - `web_search_preview` + - `computer_use_preview` + enum: + - file_search + - web_search_preview + - computer_use_preview + - web_search_preview_2025_03_11 + required: + - type + ToolChoiceOptions: + type: string + title: Tool choice mode + description: > + Controls which (if any) tool is called by the model. + + + `none` means the model will not call any tool and instead generates a + message. + + + `auto` means the model can pick between generating a message or calling + one or + + more tools. + + + `required` means the model must call one or more tools. + enum: + - none + - auto + - required + TextResponseFormatConfiguration: + description: > + An object specifying the format that the model must output. + + + Configuring `{ "type": "json_schema" }` enables Structured Outputs, + + which ensures the model will match your supplied JSON schema. Learn more + in the + + [Structured Outputs guide](/docs/guides/structured-outputs). + + + The default format is `{ "type": "text" }` with no additional options. + + + **Not recommended for gpt-4o and newer models:** + + + Setting to `{ "type": "json_object" }` enables the older JSON mode, + which + + ensures the message the model generates is valid JSON. Using + `json_schema` + + is preferred for models that support it. + oneOf: + - $ref: "#/components/schemas/ResponseFormatText" + - $ref: "#/components/schemas/TextResponseFormatJsonSchema" + - $ref: "#/components/schemas/ResponseFormatJsonObject" + CreateResponse: + allOf: + - $ref: "#/components/schemas/CreateModelResponseProperties" + - $ref: "#/components/schemas/ResponseProperties" + - type: object + properties: + input: + description: > + Text, image, or file inputs to the model, used to generate a + response. + + + Learn more: + + - [Text inputs and outputs](/docs/guides/text) + + - [Image inputs](/docs/guides/images) + + - [File inputs](/docs/guides/pdf-files) + + - [Conversation state](/docs/guides/conversation-state) + + - [Function calling](/docs/guides/function-calling) + oneOf: + - type: string + title: Text input + description: > + A text input to the model, equivalent to a text input with + the + + `user` role. + - type: array + title: Input item list + description: | + A list of one or many input items to the model, containing + different content types. + items: + $ref: "#/components/schemas/InputItem" + include: + type: array + description: > + Specify additional output data to include in the model response. + Currently + + supported values are: + + - `file_search_call.results`: Include the search results of + the file search tool call. + - `message.input_image.image_url`: Include image urls from the + input message. + + - `computer_call_output.output.image_url`: Include image urls + from the computer call output. + items: + $ref: "#/components/schemas/Includable" + nullable: true + parallel_tool_calls: + type: boolean + description: | + Whether to allow the model to run tool calls in parallel. + default: true + nullable: true + store: + type: boolean + description: > + Whether to store the generated model response for later + retrieval via + + API. + default: true + nullable: true + stream: + description: | + If set to true, the model response data will be streamed to the client + as it is generated using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format). + See the [Streaming section below](/docs/api-reference/responses-streaming) + for more information. + type: boolean + nullable: true + default: false + required: + - model + - input + CreateModelResponseProperties: + allOf: + - $ref: "#/components/schemas/ModelResponseProperties" + InputItem: + oneOf: + - $ref: "#/components/schemas/EasyInputMessage" + - type: object + title: Item + description: | + An item representing part of the context for the response to be + generated by the model. Can contain text, images, and audio inputs, + as well as previous assistant responses and tool call outputs. + $ref: "#/components/schemas/Item" + - $ref: "#/components/schemas/ItemReferenceParam" + discriminator: + propertyName: type + Item: + type: object + description: | + Content item used to generate a response. + oneOf: + - $ref: "#/components/schemas/InputMessage" + - $ref: "#/components/schemas/OutputMessage" + - $ref: "#/components/schemas/FileSearchToolCall" + - $ref: "#/components/schemas/ComputerToolCall" + - $ref: "#/components/schemas/ComputerCallOutputItemParam" + - $ref: "#/components/schemas/WebSearchToolCall" + - $ref: "#/components/schemas/FunctionToolCall" + - $ref: "#/components/schemas/FunctionCallOutputItemParam" + - $ref: "#/components/schemas/ReasoningItem" + discriminator: + propertyName: type + ItemReferenceParam: + properties: + type: + anyOf: + - type: string + enum: + - item_reference + description: The type of item to reference. Always `item_reference`. + default: item_reference + x-stainless-const: true + id: + type: string + description: The ID of the item to reference. + type: object + required: + - id + title: Item reference + description: An internal identifier for an item to reference. + ItemResource: + description: | + Content item used to generate a response. + oneOf: + - $ref: "#/components/schemas/InputMessageResource" + - $ref: "#/components/schemas/OutputMessage" + - $ref: "#/components/schemas/FileSearchToolCall" + - $ref: "#/components/schemas/ComputerToolCall" + - $ref: "#/components/schemas/ComputerToolCallOutputResource" + - $ref: "#/components/schemas/WebSearchToolCall" + - $ref: "#/components/schemas/FunctionToolCallResource" + - $ref: "#/components/schemas/FunctionToolCallOutputResource" + discriminator: + propertyName: type + ComputerCallOutputItemParam: + properties: + id: + anyOf: + - type: string + description: The ID of the computer tool call output. + call_id: + type: string + maxLength: 64 + minLength: 1 + description: The ID of the computer tool call that produced the output. + type: + type: string + enum: + - computer_call_output + description: The type of the computer tool call output. Always + `computer_call_output`. + default: computer_call_output + x-stainless-const: true + output: + $ref: "#/components/schemas/ComputerScreenshotImage" + acknowledged_safety_checks: + anyOf: + - items: + $ref: "#/components/schemas/ComputerCallSafetyCheckParam" + type: array + description: The safety checks reported by the API that have been acknowledged + by the developer. + status: + anyOf: + - type: string + enum: + - in_progress + - completed + - incomplete + description: The status of the message input. One of `in_progress`, `completed`, + or `incomplete`. Populated when input items are returned via + API. + type: object + required: + - call_id + - type + - output + title: Computer tool call output + description: The output of a computer tool call. + TextResponseFormatJsonSchema: + type: object + title: JSON schema + description: | + JSON Schema response format. Used to generate structured JSON responses. + Learn more about [Structured Outputs](/docs/guides/structured-outputs). + properties: + type: + type: string + description: The type of response format being defined. Always `json_schema`. + enum: + - json_schema + x-stainless-const: true + description: + type: string + description: > + A description of what the response format is for, used by the model + to + + determine how to respond in the format. + name: + type: string + description: | + The name of the response format. Must be a-z, A-Z, 0-9, or contain + underscores and dashes, with a maximum length of 64. + schema: + $ref: "#/components/schemas/ResponseFormatJsonSchemaSchema" + strict: + type: boolean + nullable: true + default: false + description: > + Whether to enable strict schema adherence when generating the + output. + + If set to true, the model will always follow the exact schema + defined + + in the `schema` field. Only a subset of JSON Schema is supported + when + + `strict` is `true`. To learn more, read the [Structured Outputs + + guide](/docs/guides/structured-outputs). + required: + - type + - schema + - name + EasyInputMessage: + type: object + title: Input message + description: > + A message input to the model with a role indicating instruction + following + + hierarchy. Instructions given with the `developer` or `system` role take + + precedence over instructions given with the `user` role. Messages with + the + + `assistant` role are presumed to have been generated by the model in + previous + + interactions. + properties: + role: + type: string + description: > + The role of the message input. One of `user`, `assistant`, `system`, + or + + `developer`. + enum: + - user + - assistant + - system + - developer + content: + description: > + Text, image, or audio input to the model, used to generate a + response. + + Can also contain previous assistant responses. + oneOf: + - type: string + title: Text input + description: | + A text input to the model. + - $ref: "#/components/schemas/InputMessageContentList" + type: + type: string + description: | + The type of the message input. Always `message`. + enum: + - message + x-stainless-const: true + required: + - role + - content + InputMessageResource: + allOf: + - $ref: "#/components/schemas/InputMessage" + - type: object + properties: + id: + type: string + description: | + The unique ID of the message input. + required: + - id + InputMessage: + type: object + title: Input message + description: > + A message input to the model with a role indicating instruction + following + + hierarchy. Instructions given with the `developer` or `system` role take + + precedence over instructions given with the `user` role. + properties: + type: + type: string + description: | + The type of the message input. Always set to `message`. + enum: + - message + x-stainless-const: true + role: + type: string + description: > + The role of the message input. One of `user`, `system`, or + `developer`. + enum: + - user + - system + - developer + status: + type: string + description: | + The status of item. One of `in_progress`, `completed`, or + `incomplete`. Populated when items are returned via API. + enum: + - in_progress + - completed + - incomplete + content: + $ref: "#/components/schemas/InputMessageContentList" + required: + - role + - content + ComputerCallSafetyCheckParam: + properties: + id: + type: string + description: The ID of the pending safety check. + code: + anyOf: + - type: string + description: The type of the pending safety check. + message: + anyOf: + - type: string + description: Details about the pending safety check. + type: object + required: + - id + description: A pending safety check for the computer call. + InputMessageContentList: + type: array + title: Input item content list + description: > + A list of one or many input items to the model, containing different + content + + types. + items: + $ref: "#/components/schemas/InputContent" + Includable: + type: string + description: > + Specify additional output data to include in the model response. + Currently + + supported values are: + + - `file_search_call.results`: Include the search results of + the file search tool call. + - `message.input_image.image_url`: Include image urls from the input + message. + + - `computer_call_output.output.image_url`: Include image urls from the + computer call output. + enum: + - file_search_call.results + - message.input_image.image_url + - computer_call_output.output.image_url + ComputerToolCallOutputResource: + allOf: + - $ref: "#/components/schemas/ComputerToolCallOutput" + - type: object + properties: + id: + type: string + description: | + The unique ID of the computer call tool output. + required: + - id + ComputerToolCallOutput: + type: object + title: Computer tool call output + description: | + The output of a computer tool call. + properties: + type: + type: string + description: > + The type of the computer tool call output. Always + `computer_call_output`. + enum: + - computer_call_output + default: computer_call_output + x-stainless-const: true + id: + type: string + description: | + The ID of the computer tool call output. + call_id: + type: string + description: | + The ID of the computer tool call that produced the output. + acknowledged_safety_checks: + type: array + description: > + The safety checks reported by the API that have been acknowledged by + the + + developer. + items: + $ref: "#/components/schemas/ComputerToolCallSafetyCheck" + output: + $ref: "#/components/schemas/ComputerScreenshotImage" + status: + type: string + description: > + The status of the message input. One of `in_progress`, `completed`, + or + + `incomplete`. Populated when input items are returned via API. + enum: + - in_progress + - completed + - incomplete + required: + - type + - call_id + - output + InputContent: + oneOf: + - $ref: "#/components/schemas/InputTextContent" + - $ref: "#/components/schemas/InputImageContent" + - $ref: "#/components/schemas/InputFileContent" + ComputerScreenshotImage: + type: object + description: | + A computer screenshot image used with the computer use tool. + properties: + type: + type: string + enum: + - computer_screenshot + default: computer_screenshot + description: > + Specifies the event type. For a computer screenshot, this property + is + + always set to `computer_screenshot`. + x-stainless-const: true + image_url: + type: string + description: The URL of the screenshot image. + file_id: + type: string + description: The identifier of an uploaded file that contains the screenshot. + required: + - type + InputTextContent: + properties: + type: + type: string + enum: + - input_text + description: The type of the input item. Always `input_text`. + default: input_text + x-stainless-const: true + text: + type: string + description: The text input to the model. + type: object + required: + - type + - text + title: Input text + description: A text input to the model. + InputImageContent: + properties: + type: + type: string + enum: + - input_image + description: The type of the input item. Always `input_image`. + default: input_image + x-stainless-const: true + image_url: + anyOf: + - type: string + description: The URL of the image to be sent to the model. A fully qualified URL + or base64 encoded image in a data URL. + file_id: + anyOf: + - type: string + description: The ID of the file to be sent to the model. + detail: + type: string + enum: + - low + - high + - auto + description: The detail level of the image to be sent to the model. One of + `high`, `low`, or `auto`. Defaults to `auto`. + type: object + required: + - type + - detail + title: Input image + description: An image input to the model. Learn about [image + inputs](/docs/guides/vision). + InputFileContent: + properties: + type: + type: string + enum: + - input_file + description: The type of the input item. Always `input_file`. + default: input_file + x-stainless-const: true + file_id: + anyOf: + - type: string + description: The ID of the file to be sent to the model. + filename: + type: string + description: The name of the file to be sent to the model. + file_data: + type: string + description: | + The content of the file to be sent to the model. + type: object + required: &a1 + - type + title: Input file + description: A file input to the model. + FunctionCallOutputItemParam: + properties: + id: + anyOf: + - type: string + description: The unique ID of the function tool call output. Populated when this + item is returned via API. + call_id: + type: string + maxLength: 64 + minLength: 1 + description: The unique ID of the function tool call generated by the model. + type: + type: string + enum: + - function_call_output + description: The type of the function tool call output. Always + `function_call_output`. + default: function_call_output + x-stainless-const: true + output: + type: string + maxLength: 10485760 + description: A JSON string of the output of the function tool call. + status: + anyOf: + - type: string + enum: + - in_progress + - completed + - incomplete + description: The status of the item. One of `in_progress`, `completed`, or + `incomplete`. Populated when items are returned via API. + type: object + required: + - call_id + - type + - output + title: Function tool call output + description: The output of a function tool call. + FunctionToolCallOutputResource: + allOf: + - $ref: "#/components/schemas/FunctionToolCallOutput" + - type: object + properties: + id: + type: string + description: | + The unique ID of the function call tool output. + required: + - id + FunctionToolCallOutput: + type: object + title: Function tool call output + description: | + The output of a function tool call. + properties: + id: + type: string + description: > + The unique ID of the function tool call output. Populated when this + item + + is returned via API. + type: + type: string + enum: + - function_call_output + description: > + The type of the function tool call output. Always + `function_call_output`. + x-stainless-const: true + call_id: + type: string + description: | + The unique ID of the function tool call generated by the model. + output: + type: string + description: | + A JSON string of the output of the function tool call. + status: + type: string + description: | + The status of the item. One of `in_progress`, `completed`, or + `incomplete`. Populated when items are returned via API. + enum: + - in_progress + - completed + - incomplete + required: + - type + - call_id + - output + FunctionToolCallResource: + allOf: + - $ref: "#/components/schemas/FunctionToolCall" + - type: object + properties: + id: + type: string + description: | + The unique ID of the function tool call. + required: + - id + CreateCompletionRequest: + type: object + properties: + model: + description: | + ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models/overview) for descriptions of them. + anyOf: + - type: string + - type: string + enum: + - gpt-3.5-turbo-instruct + - davinci-002 + - babbage-002 + x-oaiTypeLabel: string + prompt: + description: | + The prompt(s) to generate completions for, encoded as a string, array of strings, array of tokens, or array of token arrays. + + Note that <|endoftext|> is the document separator that the model sees during training, so if a prompt is not specified the model will generate as if from the beginning of a new document. + default: '<|endoftext|>' + nullable: true + oneOf: + - type: string + default: '' + example: This is a test. + - type: array + items: + type: string + default: '' + example: This is a test. + - type: array + minItems: 1 + items: + type: integer + example: '[1212, 318, 257, 1332, 13]' + - type: array + minItems: 1 + items: + type: array + minItems: 1 + items: + type: integer + example: '[[1212, 318, 257, 1332, 13]]' + best_of: + type: integer + default: 1 + minimum: 0 + maximum: 20 + nullable: true + description: | + Generates `best_of` completions server-side and returns the "best" (the one with the highest log probability per token). Results cannot be streamed. + + When used with `n`, `best_of` controls the number of candidate completions and `n` specifies how many to return – `best_of` must be greater than `n`. + + **Note:** Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for `max_tokens` and `stop`. + echo: + type: boolean + default: false + nullable: true + description: | + Echo back the prompt in addition to the completion + frequency_penalty: + type: number + default: 0 + minimum: -2 + maximum: 2 + nullable: true + description: | + Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. + + [See more information about frequency and presence penalties.](/docs/guides/text-generation/parameter-details) + logit_bias: + type: object + x-oaiTypeLabel: map + default: {} + nullable: true + additionalProperties: + type: integer + description: | + Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this [tokenizer tool](/tokenizer?view=bpe) to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. + + As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token from being generated. + logprobs: + type: integer + minimum: 0 + maximum: 5 + default: 0 + nullable: true + description: | + Include the log probabilities on the `logprobs` most likely output tokens, as well the chosen tokens. For example, if `logprobs` is 5, the API will return a list of the 5 most likely tokens. The API will always return the `logprob` of the sampled token, so there may be up to `logprobs+1` elements in the response. + + The maximum value for `logprobs` is 5. + max_tokens: + type: integer + minimum: 0 + default: 16 + example: 16 + nullable: true + description: | + The maximum number of [tokens](/tokenizer) that can be generated in the completion. + + The token count of your prompt plus `max_tokens` cannot exceed the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens. + 'n': + type: integer + minimum: 1 + maximum: 128 + default: 1 + example: 1 + nullable: true + description: | + How many completions to generate for each prompt. + + **Note:** Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for `max_tokens` and `stop`. + presence_penalty: + type: number + default: 0 + minimum: -2 + maximum: 2 + nullable: true + description: | + Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. + + [See more information about frequency and presence penalties.](/docs/guides/text-generation/parameter-details) + seed: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + nullable: true + description: | + If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result. + + Determinism is not guaranteed, and you should refer to the `system_fingerprint` response parameter to monitor changes in the backend. + stop: + description: | + Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. + default: [] + nullable: true + oneOf: + - type: string + default: <|endoftext|> + example: |+ + + nullable: true + - type: array + minItems: 1 + maxItems: 4 + items: + type: string + example: '["\n"]' + stream: + description: | + Whether to stream back partial progress. If set, tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + type: boolean + nullable: true + default: false + stream_options: + $ref: '#/components/schemas/ChatCompletionStreamOptions' + suffix: + description: | + The suffix that comes after a completion of inserted text. + + This parameter is only supported for `gpt-3.5-turbo-instruct`. + default: null + nullable: true + type: string + example: test. + temperature: + type: number + minimum: 0 + maximum: 2 + default: 1 + example: 1 + nullable: true + description: | + What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + top_p: + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + nullable: true + description: | + An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + user: + type: string + example: user-1234 + description: | + A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids). + required: + - model + - prompt + ResponseStreamEvent: + anyOf: + - $ref: "#/components/schemas/ResponseAudioDeltaEvent" + - $ref: "#/components/schemas/ResponseAudioDoneEvent" + - $ref: "#/components/schemas/ResponseAudioTranscriptDeltaEvent" + - $ref: "#/components/schemas/ResponseAudioTranscriptDoneEvent" + - $ref: "#/components/schemas/ResponseCodeInterpreterCallCodeDeltaEvent" + - $ref: "#/components/schemas/ResponseCodeInterpreterCallCodeDoneEvent" + - $ref: "#/components/schemas/ResponseCodeInterpreterCallCompletedEvent" + - $ref: "#/components/schemas/ResponseCodeInterpreterCallInProgressEvent" + - $ref: "#/components/schemas/ResponseCodeInterpreterCallInterpretingEvent" + - $ref: "#/components/schemas/ResponseCompletedEvent" + - $ref: "#/components/schemas/ResponseContentPartAddedEvent" + - $ref: "#/components/schemas/ResponseContentPartDoneEvent" + - $ref: "#/components/schemas/ResponseCreatedEvent" + - $ref: "#/components/schemas/ResponseErrorEvent" + - $ref: "#/components/schemas/ResponseFileSearchCallCompletedEvent" + - $ref: "#/components/schemas/ResponseFileSearchCallInProgressEvent" + - $ref: "#/components/schemas/ResponseFileSearchCallSearchingEvent" + - $ref: "#/components/schemas/ResponseFunctionCallArgumentsDeltaEvent" + - $ref: "#/components/schemas/ResponseFunctionCallArgumentsDoneEvent" + - $ref: "#/components/schemas/ResponseInProgressEvent" + - $ref: "#/components/schemas/ResponseFailedEvent" + - $ref: "#/components/schemas/ResponseIncompleteEvent" + - $ref: "#/components/schemas/ResponseOutputItemAddedEvent" + - $ref: "#/components/schemas/ResponseOutputItemDoneEvent" + - $ref: "#/components/schemas/ResponseReasoningSummaryPartAddedEvent" + - $ref: "#/components/schemas/ResponseReasoningSummaryPartDoneEvent" + - $ref: "#/components/schemas/ResponseReasoningSummaryTextDeltaEvent" + - $ref: "#/components/schemas/ResponseReasoningSummaryTextDoneEvent" + - $ref: "#/components/schemas/ResponseRefusalDeltaEvent" + - $ref: "#/components/schemas/ResponseRefusalDoneEvent" + - $ref: "#/components/schemas/ResponseTextAnnotationDeltaEvent" + - $ref: "#/components/schemas/ResponseTextDeltaEvent" + - $ref: "#/components/schemas/ResponseTextDoneEvent" + - $ref: "#/components/schemas/ResponseWebSearchCallCompletedEvent" + - $ref: "#/components/schemas/ResponseWebSearchCallInProgressEvent" + - $ref: "#/components/schemas/ResponseWebSearchCallSearchingEvent" + discriminator: + propertyName: type + ResponseWebSearchCallSearchingEvent: + type: object + description: Emitted when a web search call is executing. + properties: + type: + type: string + description: | + The type of the event. Always `response.web_search_call.searching`. + enum: + - response.web_search_call.searching + x-stainless-const: true + output_index: + type: integer + description: > + The index of the output item that the web search call is associated + with. + item_id: + type: string + description: | + Unique ID for the output item associated with the web search call. + required: + - type + - output_index + - item_id + x-oaiMeta: + name: response.web_search_call.searching + group: responses + example: | + { + "type": "response.web_search_call.searching", + "output_index": 0, + "item_id": "ws_123", + } + ResponseFileSearchCallSearchingEvent: + type: object + description: Emitted when a file search is currently searching. + properties: + type: + type: string + description: | + The type of the event. Always `response.file_search_call.searching`. + enum: + - response.file_search_call.searching + x-stainless-const: true + output_index: + type: integer + description: | + The index of the output item that the file search call is searching. + item_id: + type: string + description: | + The ID of the output item that the file search call is initiated. + required: + - type + - output_index + - item_id + x-oaiMeta: + name: response.file_search_call.searching + group: responses + example: | + { + "type": "response.file_search_call.searching", + "output_index": 0, + "item_id": "fs_123", + } + ResponseFileSearchCallCompletedEvent: + type: object + description: Emitted when a file search call is completed (results found). + properties: + type: + type: string + description: | + The type of the event. Always `response.file_search_call.completed`. + enum: + - response.file_search_call.completed + x-stainless-const: true + output_index: + type: integer + description: | + The index of the output item that the file search call is initiated. + item_id: + type: string + description: | + The ID of the output item that the file search call is initiated. + required: + - type + - output_index + - item_id + x-oaiMeta: + name: response.file_search_call.completed + group: responses + example: | + { + "type": "response.file_search_call.completed", + "output_index": 0, + "item_id": "fs_123", + } + ResponseFileSearchCallInProgressEvent: + type: object + description: Emitted when a file search call is initiated. + properties: + type: + type: string + description: > + The type of the event. Always + `response.file_search_call.in_progress`. + enum: + - response.file_search_call.in_progress + x-stainless-const: true + output_index: + type: integer + description: | + The index of the output item that the file search call is initiated. + item_id: + type: string + description: | + The ID of the output item that the file search call is initiated. + required: + - type + - output_index + - item_id + x-oaiMeta: + name: response.file_search_call.in_progress + group: responses + example: | + { + "type": "response.file_search_call.in_progress", + "output_index": 0, + "item_id": "fs_123", + } + ResponseFailedEvent: + type: object + description: | + An event that is emitted when a response fails. + properties: + type: + type: string + description: | + The type of the event. Always `response.failed`. + enum: + - response.failed + x-stainless-const: true + response: + $ref: "#/components/schemas/Response" + description: | + The response that failed. + required: + - type + - response + x-oaiMeta: + name: response.failed + group: responses + example: | + { + "type": "response.failed", + "response": { + "id": "resp_123", + "object": "response", + "created_at": 1740855869, + "status": "failed", + "error": { + "code": "server_error", + "message": "The model failed to generate a response." + }, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [], + "previous_response_id": null, + "reasoning_effort": null, + "store": false, + "temperature": 1, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1, + "truncation": "disabled", + "usage": null, + "user": null, + "metadata": {} + } + } + ResponseFunctionCallArgumentsDeltaEvent: + type: object + description: Emitted when there is a partial function-call arguments delta. + properties: + type: + type: string + description: > + The type of the event. Always + `response.function_call_arguments.delta`. + enum: + - response.function_call_arguments.delta + x-stainless-const: true + item_id: + type: string + description: > + The ID of the output item that the function-call arguments delta is + added to. + output_index: + type: integer + description: > + The index of the output item that the function-call arguments delta + is added to. + delta: + type: string + description: | + The function-call arguments delta that is added. + required: + - type + - item_id + - output_index + - delta + x-oaiMeta: + name: response.function_call_arguments.delta + group: responses + example: | + { + "type": "response.function_call_arguments.delta", + "item_id": "item-abc", + "output_index": 0, + "delta": "{ \"arg\":" + } + ResponseFunctionCallArgumentsDoneEvent: + type: object + description: Emitted when function-call arguments are finalized. + properties: + type: + type: string + enum: + - response.function_call_arguments.done + x-stainless-const: true + item_id: + type: string + description: The ID of the item. + output_index: + type: integer + description: The index of the output item. + arguments: + type: string + description: The function-call arguments. + required: + - type + - item_id + - output_index + - arguments + x-oaiMeta: + name: response.function_call_arguments.done + group: responses + example: | + { + "type": "response.function_call_arguments.done", + "item_id": "item-abc", + "output_index": 1, + "arguments": "{ \"arg\": 123 }" + } + ResponseCompletedEvent: + type: object + description: Emitted when the model response is complete. + properties: + type: + type: string + description: | + The type of the event. Always `response.completed`. + enum: + - response.completed + x-stainless-const: true + response: + $ref: "#/components/schemas/Response" + description: | + Properties of the completed response. + required: + - type + - response + x-oaiMeta: + name: response.completed + group: responses + example: > + { + "type": "response.completed", + "response": { + "id": "resp_123", + "object": "response", + "created_at": 1740855869, + "status": "completed", + "error": null, + "incomplete_details": null, + "input": [], + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_123", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "In a shimmering forest under a sky full of stars, a lonely unicorn named Lila discovered a hidden pond that glowed with moonlight. Every night, she would leave sparkling, magical flowers by the water's edge, hoping to share her beauty with others. One enchanting evening, she woke to find a group of friendly animals gathered around, eager to be friends and share in her magic.", + "annotations": [] + } + ] + } + ], + "previous_response_id": null, + "reasoning_effort": null, + "store": false, + "temperature": 1, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1, + "truncation": "disabled", + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 0 + }, + "user": null, + "metadata": {} + } + } + ResponseContentPartAddedEvent: + type: object + description: Emitted when a new content part is added. + properties: + type: + type: string + description: | + The type of the event. Always `response.content_part.added`. + enum: + - response.content_part.added + x-stainless-const: true + item_id: + type: string + description: | + The ID of the output item that the content part was added to. + output_index: + type: integer + description: | + The index of the output item that the content part was added to. + content_index: + type: integer + description: | + The index of the content part that was added. + part: + $ref: "#/components/schemas/OutputContent" + description: | + The content part that was added. + required: + - type + - item_id + - output_index + - content_index + - part + x-oaiMeta: + name: response.content_part.added + group: responses + example: | + { + "type": "response.content_part.added", + "item_id": "msg_123", + "output_index": 0, + "content_index": 0, + "part": { + "type": "output_text", + "text": "", + "annotations": [] + } + } + ResponseContentPartDoneEvent: + type: object + description: Emitted when a content part is done. + properties: + type: + type: string + description: | + The type of the event. Always `response.content_part.done`. + enum: + - response.content_part.done + x-stainless-const: true + item_id: + type: string + description: | + The ID of the output item that the content part was added to. + output_index: + type: integer + description: | + The index of the output item that the content part was added to. + content_index: + type: integer + description: | + The index of the content part that is done. + part: + $ref: "#/components/schemas/OutputContent" + description: | + The content part that is done. + required: + - type + - item_id + - output_index + - content_index + - part + x-oaiMeta: + name: response.content_part.done + group: responses + example: > + { + "type": "response.content_part.done", + "item_id": "msg_123", + "output_index": 0, + "content_index": 0, + "part": { + "type": "output_text", + "text": "In a shimmering forest under a sky full of stars, a lonely unicorn named Lila discovered a hidden pond that glowed with moonlight. Every night, she would leave sparkling, magical flowers by the water's edge, hoping to share her beauty with others. One enchanting evening, she woke to find a group of friendly animals gathered around, eager to be friends and share in her magic.", + "annotations": [] + } + } + ResponseAudioDeltaEvent: + type: object + description: Emitted when there is a partial audio response. + properties: + type: + type: string + description: | + The type of the event. Always `response.audio.delta`. + enum: + - response.audio.delta + x-stainless-const: true + delta: + type: string + description: | + A chunk of Base64 encoded response audio bytes. + required: + - type + - delta + x-oaiMeta: + name: response.audio.delta + group: responses + example: | + { + "type": "response.audio.delta", + "response_id": "resp_123", + "delta": "base64encoded..." + } + ResponseAudioDoneEvent: + type: object + description: Emitted when the audio response is complete. + properties: + type: + type: string + description: | + The type of the event. Always `response.audio.done`. + enum: + - response.audio.done + x-stainless-const: true + required: + - type + - response_id + x-oaiMeta: + name: response.audio.done + group: responses + example: | + { + "type": "response.audio.done", + "response_id": "resp-123" + } + ResponseAudioTranscriptDeltaEvent: + type: object + description: Emitted when there is a partial transcript of audio. + properties: + type: + type: string + description: | + The type of the event. Always `response.audio.transcript.delta`. + enum: + - response.audio.transcript.delta + x-stainless-const: true + delta: + type: string + description: | + The partial transcript of the audio response. + required: + - type + - response_id + - delta + x-oaiMeta: + name: response.audio.transcript.delta + group: responses + example: | + { + "type": "response.audio.transcript.delta", + "response_id": "resp_123", + "delta": " ... partial transcript ... " + } + ResponseAudioTranscriptDoneEvent: + type: object + description: Emitted when the full audio transcript is completed. + properties: + type: + type: string + description: | + The type of the event. Always `response.audio.transcript.done`. + enum: + - response.audio.transcript.done + x-stainless-const: true + required: + - type + - response_id + x-oaiMeta: + name: response.audio.transcript.done + group: responses + example: | + { + "type": "response.audio.transcript.done", + "response_id": "resp_123" + } + ResponseCodeInterpreterCallCodeDeltaEvent: + type: object + description: Emitted when a partial code snippet is added by the code interpreter. + properties: + type: + type: string + description: > + The type of the event. Always + `response.code_interpreter_call.code.delta`. + enum: + - response.code_interpreter_call.code.delta + x-stainless-const: true + output_index: + type: integer + description: > + The index of the output item that the code interpreter call is in + progress. + delta: + type: string + description: | + The partial code snippet added by the code interpreter. + required: + - type + - response_id + - output_index + - delta + x-oaiMeta: + name: response.code_interpreter_call.code.delta + group: responses + example: | + { + "type": "response.code_interpreter_call.code.delta", + "response_id": "resp-123", + "output_index": 0, + "delta": "partial code" + } + ResponseCodeInterpreterCallCodeDoneEvent: + type: object + description: Emitted when code snippet output is finalized by the code interpreter. + properties: + type: + type: string + description: > + The type of the event. Always + `response.code_interpreter_call.code.done`. + enum: + - response.code_interpreter_call.code.done + x-stainless-const: true + output_index: + type: integer + description: > + The index of the output item that the code interpreter call is in + progress. + code: + type: string + description: | + The final code snippet output by the code interpreter. + required: + - type + - response_id + - output_index + - code + x-oaiMeta: + name: response.code_interpreter_call.code.done + group: responses + example: | + { + "type": "response.code_interpreter_call.code.done", + "response_id": "resp-123", + "output_index": 3, + "code": "console.log('done');" + } + ResponseCodeInterpreterCallCompletedEvent: + type: object + description: Emitted when the code interpreter call is completed. + properties: + type: + type: string + description: > + The type of the event. Always + `response.code_interpreter_call.completed`. + enum: + - response.code_interpreter_call.completed + x-stainless-const: true + output_index: + type: integer + description: > + The index of the output item that the code interpreter call is in + progress. + code_interpreter_call: + $ref: "#/components/schemas/CodeInterpreterToolCall" + required: + - type + - response_id + - output_index + - code_interpreter_call + x-oaiMeta: + name: response.code_interpreter_call.completed + group: responses + example: | + { + "type": "response.code_interpreter_call.completed", + "response_id": "resp-123", + "output_index": 5, + "code_interpreter_call": {} + } + ResponseCodeInterpreterCallInProgressEvent: + type: object + description: Emitted when a code interpreter call is in progress. + properties: + type: + type: string + description: > + The type of the event. Always + `response.code_interpreter_call.in_progress`. + enum: + - response.code_interpreter_call.in_progress + x-stainless-const: true + output_index: + type: integer + description: > + The index of the output item that the code interpreter call is in + progress. + code_interpreter_call: + $ref: "#/components/schemas/CodeInterpreterToolCall" + required: + - type + - response_id + - output_index + - code_interpreter_call + x-oaiMeta: + name: response.code_interpreter_call.in_progress + group: responses + example: | + { + "type": "response.code_interpreter_call.in.progress", + "response_id": "resp-123", + "output_index": 0, + "code_interpreter_call": {} + } + ResponseCodeInterpreterCallInterpretingEvent: + type: object + description: Emitted when the code interpreter is actively interpreting the code + snippet. + properties: + type: + type: string + description: > + The type of the event. Always + `response.code_interpreter_call.interpreting`. + enum: + - response.code_interpreter_call.interpreting + x-stainless-const: true + output_index: + type: integer + description: > + The index of the output item that the code interpreter call is in + progress. + code_interpreter_call: + $ref: "#/components/schemas/CodeInterpreterToolCall" + required: + - type + - response_id + - output_index + - code_interpreter_call + x-oaiMeta: + name: response.code_interpreter_call.interpreting + group: responses + example: | + { + "type": "response.code_interpreter_call.interpreting", + "response_id": "resp-123", + "output_index": 4, + "code_interpreter_call": {} + } + ResponseCreatedEvent: + type: object + description: | + An event that is emitted when a response is created. + properties: + type: + type: string + description: | + The type of the event. Always `response.created`. + enum: + - response.created + x-stainless-const: true + response: + $ref: "#/components/schemas/Response" + description: | + The response that was created. + required: + - type + - response + x-oaiMeta: + name: response.created + group: responses + example: | + { + "type": "response.created", + "response": { + "id": "resp_67ccfcdd16748190a91872c75d38539e09e4d4aac714747c", + "object": "response", + "created_at": 1741487325, + "status": "in_progress", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4o-2024-08-06", + "output": [], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1, + "truncation": "disabled", + "usage": null, + "user": null, + "metadata": {} + } + } + ResponseInProgressEvent: + type: object + description: Emitted when the response is in progress. + properties: + type: + type: string + description: | + The type of the event. Always `response.in_progress`. + enum: + - response.in_progress + x-stainless-const: true + response: + $ref: "#/components/schemas/Response" + description: | + The response that is in progress. + required: + - type + - response + x-oaiMeta: + name: response.in_progress + group: responses + example: | + { + "type": "response.in_progress", + "response": { + "id": "resp_67ccfcdd16748190a91872c75d38539e09e4d4aac714747c", + "object": "response", + "created_at": 1741487325, + "status": "in_progress", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4o-2024-08-06", + "output": [], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1, + "truncation": "disabled", + "usage": null, + "user": null, + "metadata": {} + } + } + ResponseErrorEvent: + type: object + description: Emitted when an error occurs. + properties: + type: + type: string + description: | + The type of the event. Always `error`. + enum: + - error + x-stainless-const: true + code: + type: string + description: | + The error code. + nullable: true + message: + type: string + description: | + The error message. + param: + type: string + description: | + The error parameter. + nullable: true + required: + - type + - code + - message + - param + x-oaiMeta: + name: error + group: responses + example: | + { + "type": "error", + "code": "ERR_SOMETHING", + "message": "Something went wrong", + "param": null + } + ResponseWebSearchCallCompletedEvent: + type: object + description: Emitted when a web search call is completed. + properties: + type: + type: string + description: | + The type of the event. Always `response.web_search_call.completed`. + enum: + - response.web_search_call.completed + x-stainless-const: true + output_index: + type: integer + description: > + The index of the output item that the web search call is associated + with. + item_id: + type: string + description: | + Unique ID for the output item associated with the web search call. + required: + - type + - output_index + - item_id + x-oaiMeta: + name: response.web_search_call.completed + group: responses + example: | + { + "type": "response.web_search_call.completed", + "output_index": 0, + "item_id": "ws_123", + } + ResponseWebSearchCallInProgressEvent: + type: object + description: Emitted when a web search call is initiated. + properties: + type: + type: string + description: > + The type of the event. Always `response.web_search_call.in_progress`. + enum: + - response.web_search_call.in_progress + x-stainless-const: true + output_index: + type: integer + description: > + The index of the output item that the web search call is associated + with. + item_id: + type: string + description: | + Unique ID for the output item associated with the web search call. + required: + - type + - output_index + - item_id + x-oaiMeta: + name: response.web_search_call.in_progress + group: responses + example: | + { + "type": "response.web_search_call.in_progress", + "output_index": 0, + "item_id": "ws_123", + } + CodeInterpreterToolCall: + type: object + title: Code interpreter tool call + description: | + A tool call to run code. + properties: + id: + type: string + description: | + The unique ID of the code interpreter tool call. + type: + type: string + enum: + - code_interpreter_call + description: > + The type of the code interpreter tool call. Always + `code_interpreter_call`. + x-stainless-const: true + code: + type: string + description: | + The code to run. + status: + type: string + enum: + - in_progress + - interpreting + - completed + description: | + The status of the code interpreter tool call. + results: + type: array + items: + $ref: "#/components/schemas/CodeInterpreterToolOutput" + description: | + The results of the code interpreter tool call. + required: + - id + - type + - code + - status + - results + CodeInterpreterToolOutput: + oneOf: + - $ref: "#/components/schemas/CodeInterpreterTextOutput" + - $ref: "#/components/schemas/CodeInterpreterFileOutput" + CodeInterpreterFileOutput: + type: object + title: Code interpreter file output + description: | + The output of a code interpreter tool call that is a file. + properties: + type: + type: string + enum: + - files + description: | + The type of the code interpreter file output. Always `files`. + x-stainless-const: true + files: + type: array + items: + type: object + properties: + mime_type: + type: string + description: | + The MIME type of the file. + file_id: + type: string + description: | + The ID of the file. + required: + - mime_type + - file_id + required: + - type + - files + CodeInterpreterTextOutput: + type: object + title: Code interpreter text output + description: | + The output of a code interpreter tool call that is text. + properties: + type: + type: string + enum: + - logs + description: | + The type of the code interpreter text output. Always `logs`. + x-stainless-const: true + logs: + type: string + description: | + The logs of the code interpreter tool call. + required: + - type + - logs + ResponseTextDoneEvent: + type: object + description: Emitted when text content is finalized. + properties: + type: + type: string + description: | + The type of the event. Always `response.output_text.done`. + enum: + - response.output_text.done + x-stainless-const: true + item_id: + type: string + description: | + The ID of the output item that the text content is finalized. + output_index: + type: integer + description: | + The index of the output item that the text content is finalized. + content_index: + type: integer + description: | + The index of the content part that the text content is finalized. + text: + type: string + description: | + The text content that is finalized. + required: + - type + - item_id + - output_index + - content_index + - text + x-oaiMeta: + name: response.output_text.done + group: responses + example: > + { + "type": "response.output_text.done", + "item_id": "msg_123", + "output_index": 0, + "content_index": 0, + "text": "In a shimmering forest under a sky full of stars, a lonely unicorn named Lila discovered a hidden pond that glowed with moonlight. Every night, she would leave sparkling, magical flowers by the water's edge, hoping to share her beauty with others. One enchanting evening, she woke to find a group of friendly animals gathered around, eager to be friends and share in her magic." + } + ResponseTextDeltaEvent: + type: object + description: Emitted when there is an additional text delta. + properties: + type: + type: string + description: | + The type of the event. Always `response.output_text.delta`. + enum: + - response.output_text.delta + x-stainless-const: true + item_id: + type: string + description: | + The ID of the output item that the text delta was added to. + output_index: + type: integer + description: | + The index of the output item that the text delta was added to. + content_index: + type: integer + description: | + The index of the content part that the text delta was added to. + delta: + type: string + description: | + The text delta that was added. + required: + - type + - item_id + - output_index + - content_index + - delta + x-oaiMeta: + name: response.output_text.delta + group: responses + example: | + { + "type": "response.output_text.delta", + "item_id": "msg_123", + "output_index": 0, + "content_index": 0, + "delta": "In" + } + ResponseRefusalDoneEvent: + type: object + description: Emitted when refusal text is finalized. + properties: + type: + type: string + description: | + The type of the event. Always `response.refusal.done`. + enum: + - response.refusal.done + x-stainless-const: true + item_id: + type: string + description: | + The ID of the output item that the refusal text is finalized. + output_index: + type: integer + description: | + The index of the output item that the refusal text is finalized. + content_index: + type: integer + description: | + The index of the content part that the refusal text is finalized. + refusal: + type: string + description: | + The refusal text that is finalized. + required: + - type + - item_id + - output_index + - content_index + - refusal + x-oaiMeta: + name: response.refusal.done + group: responses + example: | + { + "type": "response.refusal.done", + "item_id": "item-abc", + "output_index": 1, + "content_index": 2, + "refusal": "final refusal text" + } + ResponseRefusalDeltaEvent: + type: object + description: Emitted when there is a partial refusal text. + properties: + type: + type: string + description: | + The type of the event. Always `response.refusal.delta`. + enum: + - response.refusal.delta + x-stainless-const: true + item_id: + type: string + description: | + The ID of the output item that the refusal text is added to. + output_index: + type: integer + description: | + The index of the output item that the refusal text is added to. + content_index: + type: integer + description: | + The index of the content part that the refusal text is added to. + delta: + type: string + description: | + The refusal text that is added. + required: + - type + - item_id + - output_index + - content_index + - delta + x-oaiMeta: + name: response.refusal.delta + group: responses + example: | + { + "type": "response.refusal.delta", + "item_id": "msg_123", + "output_index": 0, + "content_index": 0, + "delta": "refusal text so far" + } + ResponseReasoningSummaryPartAddedEvent: + type: object + description: Emitted when a new reasoning summary part is added. + properties: + type: + type: string + description: > + The type of the event. Always + `response.reasoning_summary_part.added`. + enum: + - response.reasoning_summary_part.added + x-stainless-const: true + item_id: + type: string + description: | + The ID of the item this summary part is associated with. + output_index: + type: integer + description: | + The index of the output item this summary part is associated with. + summary_index: + type: integer + description: | + The index of the summary part within the reasoning summary. + part: + type: object + description: | + The summary part that was added. + properties: + type: + type: string + description: The type of the summary part. Always `summary_text`. + enum: + - summary_text + x-stainless-const: true + text: + type: string + description: The text of the summary part. + required: + - type + - text + required: + - type + - item_id + - output_index + - summary_index + - part + x-oaiMeta: + name: response.reasoning_summary_part.added + group: responses + example: | + { + "type": "response.reasoning_summary_part.added", + "item_id": "rs_6806bfca0b2481918a5748308061a2600d3ce51bdffd5476", + "output_index": 0, + "summary_index": 0, + "part": { + "type": "summary_text", + "text": "" + } + } + ResponseReasoningSummaryPartDoneEvent: + type: object + description: Emitted when a reasoning summary part is completed. + properties: + type: + type: string + description: > + The type of the event. Always `response.reasoning_summary_part.done`. + enum: + - response.reasoning_summary_part.done + x-stainless-const: true + item_id: + type: string + description: | + The ID of the item this summary part is associated with. + output_index: + type: integer + description: | + The index of the output item this summary part is associated with. + summary_index: + type: integer + description: | + The index of the summary part within the reasoning summary. + part: + type: object + description: | + The completed summary part. + properties: + type: + type: string + description: The type of the summary part. Always `summary_text`. + enum: + - summary_text + x-stainless-const: true + text: + type: string + description: The text of the summary part. + required: + - type + - text + required: + - type + - item_id + - output_index + - summary_index + - part + x-oaiMeta: + name: response.reasoning_summary_part.done + group: responses + example: > + { + "type": "response.reasoning_summary_part.done", + "item_id": "rs_6806bfca0b2481918a5748308061a2600d3ce51bdffd5476", + "output_index": 0, + "summary_index": 0, + "part": { + "type": "summary_text", + "text": "**Responding to a greeting**\n\nThe user just said, \"Hello!\" So, it seems I need to engage. I'll greet them back and offer help since they're looking to chat. I could say something like, \"Hello! How can I assist you today?\" That feels friendly and open. They didn't ask a specific question, so this approach will work well for starting a conversation. Let's see where it goes from there!" + } + } + ResponseOutputItemAddedEvent: + type: object + description: Emitted when a new output item is added. + properties: + type: + type: string + description: | + The type of the event. Always `response.output_item.added`. + enum: + - response.output_item.added + x-stainless-const: true + output_index: + type: integer + description: | + The index of the output item that was added. + item: + $ref: "#/components/schemas/OutputItem" + description: | + The output item that was added. + required: + - type + - output_index + - item + x-oaiMeta: + name: response.output_item.added + group: responses + example: | + { + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": "msg_123", + "status": "in_progress", + "type": "message", + "role": "assistant", + "content": [] + } + } + ResponseOutputItemDoneEvent: + type: object + description: Emitted when an output item is marked done. + properties: + type: + type: string + description: | + The type of the event. Always `response.output_item.done`. + enum: + - response.output_item.done + x-stainless-const: true + output_index: + type: integer + description: | + The index of the output item that was marked done. + item: + $ref: "#/components/schemas/OutputItem" + description: | + The output item that was marked done. + required: + - type + - output_index + - item + x-oaiMeta: + name: response.output_item.done + group: responses + example: > + { + "type": "response.output_item.done", + "output_index": 0, + "item": { + "id": "msg_123", + "status": "completed", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "In a shimmering forest under a sky full of stars, a lonely unicorn named Lila discovered a hidden pond that glowed with moonlight. Every night, she would leave sparkling, magical flowers by the water's edge, hoping to share her beauty with others. One enchanting evening, she woke to find a group of friendly animals gathered around, eager to be friends and share in her magic.", + "annotations": [] + } + ] + } + } + ResponseIncompleteEvent: + type: object + description: | + An event that is emitted when a response finishes as incomplete. + properties: + type: + type: string + description: | + The type of the event. Always `response.incomplete`. + enum: + - response.incomplete + x-stainless-const: true + response: + $ref: "#/components/schemas/Response" + description: | + The response that was incomplete. + required: + - type + - response + x-oaiMeta: + name: response.incomplete + group: responses + example: | + { + "type": "response.incomplete", + "response": { + "id": "resp_123", + "object": "response", + "created_at": 1740855869, + "status": "incomplete", + "error": null, + "incomplete_details": { + "reason": "max_tokens" + }, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [], + "previous_response_id": null, + "reasoning_effort": null, + "store": false, + "temperature": 1, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1, + "truncation": "disabled", + "usage": null, + "user": null, + "metadata": {} + } + } + ResponseReasoningSummaryTextDeltaEvent: + type: object + description: Emitted when a delta is added to a reasoning summary text. + properties: + type: + type: string + description: > + The type of the event. Always + `response.reasoning_summary_text.delta`. + enum: + - response.reasoning_summary_text.delta + x-stainless-const: true + item_id: + type: string + description: | + The ID of the item this summary text delta is associated with. + output_index: + type: integer + description: > + The index of the output item this summary text delta is associated + with. + summary_index: + type: integer + description: | + The index of the summary part within the reasoning summary. + delta: + type: string + description: | + The text delta that was added to the summary. + required: + - type + - item_id + - output_index + - summary_index + - delta + x-oaiMeta: + name: response.reasoning_summary_text.delta + group: responses + example: | + { + "type": "response.reasoning_summary_text.delta", + "item_id": "rs_6806bfca0b2481918a5748308061a2600d3ce51bdffd5476", + "output_index": 0, + "summary_index": 0, + "delta": "**Respond" + } + ResponseReasoningSummaryTextDoneEvent: + type: object + description: Emitted when a reasoning summary text is completed. + properties: + type: + type: string + description: > + The type of the event. Always `response.reasoning_summary_text.done`. + enum: + - response.reasoning_summary_text.done + x-stainless-const: true + item_id: + type: string + description: | + The ID of the item this summary text is associated with. + output_index: + type: integer + description: | + The index of the output item this summary text is associated with. + summary_index: + type: integer + description: | + The index of the summary part within the reasoning summary. + text: + type: string + description: | + The full text of the completed reasoning summary. + required: + - type + - item_id + - output_index + - summary_index + - text + x-oaiMeta: + name: response.reasoning_summary_text.done + group: responses + example: > + { + "type": "response.reasoning_summary_text.done", + "item_id": "rs_6806bfca0b2481918a5748308061a2600d3ce51bdffd5476", + "output_index": 0, + "summary_index": 0, + "text": "**Responding to a greeting**\n\nThe user just said, \"Hello!\" So, it seems I need to engage. I'll greet them back and offer help since they're looking to chat. I could say something like, \"Hello! How can I assist you today?\" That feels friendly and open. They didn't ask a specific question, so this approach will work well for starting a conversation. Let's see where it goes from there!" + } + ResponseTextAnnotationDeltaEvent: + type: object + description: Emitted when a text annotation is added. + properties: + type: + type: string + description: > + The type of the event. Always + `response.output_text.annotation.added`. + enum: + - response.output_text.annotation.added + x-stainless-const: true + item_id: + type: string + description: | + The ID of the output item that the text annotation was added to. + output_index: + type: integer + description: | + The index of the output item that the text annotation was added to. + content_index: + type: integer + description: | + The index of the content part that the text annotation was added to. + annotation_index: + type: integer + description: | + The index of the annotation that was added. + annotation: + $ref: "#/components/schemas/Annotation" + required: + - type + - item_id + - output_index + - content_index + - annotation_index + - annotation + x-oaiMeta: + name: response.output_text.annotation.added + group: responses + example: | + { + "type": "response.output_text.annotation.added", + "item_id": "msg_abc123", + "output_index": 1, + "content_index": 0, + "annotation_index": 0, + "annotation": { + "type": "file_citation", + "index": 390, + "file_id": "file-4wDz5b167pAf72nx1h9eiN", + "filename": "dragons.pdf" + } + } + CreateCompletionResponse: + type: object + description: | + Represents a completion response from the API. Note: both the streamed and non-streamed response objects share the same shape (unlike the chat endpoint). + properties: + id: + type: string + description: A unique identifier for the completion. + choices: + type: array + description: The list of completion choices the model generated for the input prompt. + items: + type: object + required: + - finish_reason + - index + - logprobs + - text + properties: + finish_reason: + type: string + description: | + The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence, + `length` if the maximum number of tokens specified in the request was reached, + or `content_filter` if content was omitted due to a flag from our content filters. + enum: + - stop + - length + - content_filter + index: + type: integer + logprobs: + type: object + nullable: true + properties: + text_offset: + type: array + items: + type: integer + token_logprobs: + type: array + items: + type: number + tokens: + type: array + items: + type: string + top_logprobs: + type: array + items: + type: object + additionalProperties: + type: number + text: + type: string + created: + type: integer + description: The Unix timestamp (in seconds) of when the completion was created. + model: + type: string + description: The model used for completion. + system_fingerprint: + type: string + description: | + This fingerprint represents the backend configuration that the model runs with. + + Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. + object: + type: string + description: The object type, which is always "text_completion" + enum: + - text_completion + usage: + $ref: '#/components/schemas/CompletionUsage' + required: + - id + - object + - created + - model + - choices + x-oaiMeta: + name: The completion object + legacy: true + example: | + { + "id": "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7", + "object": "text_completion", + "created": 1589478378, + "model": "gpt-4-turbo", + "choices": [ + { + "text": "\n\nThis is indeed a test", + "index": 0, + "logprobs": null, + "finish_reason": "length" + } + ], + "usage": { + "prompt_tokens": 5, + "completion_tokens": 7, + "total_tokens": 12 + } + } + ChatCompletionRequestMessageContentPartText: + type: object + title: Text content part + properties: + type: + type: string + enum: + - text + description: The type of the content part. + text: + type: string + description: The text content. + required: + - type + - text + ChatCompletionRequestMessageContentPartImage: + type: object + title: Image content part + properties: + type: + type: string + enum: + - image_url + description: The type of the content part. + image_url: + type: object + properties: + url: + type: string + description: Either a URL of the image or the base64 encoded image data. + format: uri + detail: + type: string + description: Specifies the detail level of the image. Learn more in the [Vision guide](/docs/guides/vision/low-or-high-fidelity-image-understanding). + enum: + - auto + - low + - high + default: auto + required: + - url + required: + - type + - image_url + ChatCompletionRequestMessageContentPartRefusal: + type: object + title: Refusal content part + properties: + type: + type: string + enum: + - refusal + description: The type of the content part. + refusal: + type: string + description: The refusal message generated by the model. + required: + - type + - refusal + ChatCompletionRequestMessage: + oneOf: + - $ref: '#/components/schemas/ChatCompletionRequestSystemMessage' + - $ref: '#/components/schemas/ChatCompletionRequestUserMessage' + - $ref: '#/components/schemas/ChatCompletionRequestAssistantMessage' + - $ref: '#/components/schemas/ChatCompletionRequestToolMessage' + - $ref: '#/components/schemas/ChatCompletionRequestFunctionMessage' + x-oaiExpandable: true + ChatCompletionRequestSystemMessageContentPart: + oneOf: + - $ref: '#/components/schemas/ChatCompletionRequestMessageContentPartText' + x-oaiExpandable: true + ChatCompletionRequestUserMessageContentPart: + oneOf: + - $ref: '#/components/schemas/ChatCompletionRequestMessageContentPartText' + - $ref: '#/components/schemas/ChatCompletionRequestMessageContentPartImage' + x-oaiExpandable: true + ChatCompletionRequestAssistantMessageContentPart: + oneOf: + - $ref: '#/components/schemas/ChatCompletionRequestMessageContentPartText' + - $ref: '#/components/schemas/ChatCompletionRequestMessageContentPartRefusal' + x-oaiExpandable: true + ChatCompletionRequestToolMessageContentPart: + oneOf: + - $ref: '#/components/schemas/ChatCompletionRequestMessageContentPartText' + x-oaiExpandable: true + ChatCompletionRequestSystemMessage: + type: object + title: System message + properties: + content: + description: The contents of the system message. + oneOf: + - type: string + description: The contents of the system message. + title: Text content + - type: array + description: An array of content parts with a defined type. For system messages, only type `text` is supported. + title: Array of content parts + items: + $ref: '#/components/schemas/ChatCompletionRequestSystemMessageContentPart' + minItems: 1 + role: + type: string + enum: + - system + description: The role of the messages author, in this case `system`. + name: + type: string + description: An optional name for the participant. Provides the model information to differentiate between participants of the same role. + required: + - content + - role + ChatCompletionRequestUserMessage: + type: object + title: User message + properties: + content: + description: | + The contents of the user message. + oneOf: + - type: string + description: The text contents of the message. + title: Text content + - type: array + description: An array of content parts with a defined type, each can be of type `text` or `image_url` when passing in images. You can pass multiple images by adding multiple `image_url` content parts. Image input is only supported when using the `gpt-4o` model. + title: Array of content parts + items: + $ref: '#/components/schemas/ChatCompletionRequestUserMessageContentPart' + minItems: 1 + x-oaiExpandable: true + role: + type: string + enum: + - user + description: The role of the messages author, in this case `user`. + name: + type: string + description: An optional name for the participant. Provides the model information to differentiate between participants of the same role. + required: + - content + - role + ChatCompletionRequestAssistantMessage: + type: object + title: Assistant message + properties: + content: + nullable: true + oneOf: + - type: string + description: The contents of the assistant message. + title: Text content + - type: array + description: An array of content parts with a defined type. Can be one or more of type `text`, or exactly one of type `refusal`. + title: Array of content parts + items: + $ref: '#/components/schemas/ChatCompletionRequestAssistantMessageContentPart' + minItems: 1 + description: | + The contents of the assistant message. Required unless `tool_calls` or `function_call` is specified. + refusal: + nullable: true + type: string + description: The refusal message by the assistant. + role: + type: string + enum: + - assistant + description: The role of the messages author, in this case `assistant`. + name: + type: string + description: An optional name for the participant. Provides the model information to differentiate between participants of the same role. + tool_calls: + $ref: '#/components/schemas/ChatCompletionMessageToolCalls' + function_call: + type: object + deprecated: true + description: Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + nullable: true + properties: + arguments: + type: string + description: The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function. + name: + type: string + description: The name of the function to call. + required: + - arguments + - name + required: + - role + FineTuneChatCompletionRequestAssistantMessage: + allOf: + - type: object + title: Assistant message + deprecated: false + properties: + weight: + type: integer + enum: + - 0 + - 1 + description: Controls whether the assistant message is trained against (0 or 1) + - $ref: '#/components/schemas/ChatCompletionRequestAssistantMessage' + required: + - role + ChatCompletionRequestToolMessage: + type: object + title: Tool message + properties: + role: + type: string + enum: + - tool + description: The role of the messages author, in this case `tool`. + content: + oneOf: + - type: string + description: The contents of the tool message. + title: Text content + - type: array + description: An array of content parts with a defined type. For tool messages, only type `text` is supported. + title: Array of content parts + items: + $ref: '#/components/schemas/ChatCompletionRequestToolMessageContentPart' + minItems: 1 + description: The contents of the tool message. + tool_call_id: + type: string + description: Tool call that this message is responding to. + required: + - role + - content + - tool_call_id + ChatCompletionRequestFunctionMessage: + type: object + title: Function message + deprecated: true + properties: + role: + type: string + enum: + - function + description: The role of the messages author, in this case `function`. + content: + nullable: true + type: string + description: The contents of the function message. + name: + type: string + description: The name of the function to call. + required: + - role + - content + - name + FunctionParameters: + type: object + description: |- + The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. + + Omitting `parameters` defines a function with an empty parameter list. + additionalProperties: true + ChatCompletionFunctions: + type: object + deprecated: true + properties: + description: + type: string + description: A description of what the function does, used by the model to choose when and how to call the function. + name: + type: string + description: The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + parameters: + $ref: '#/components/schemas/FunctionParameters' + required: + - name + ChatCompletionFunctionCallOption: + type: object + description: | + Specifying a particular function via `{"name": "my_function"}` forces the model to call that function. + properties: + name: + type: string + description: The name of the function to call. + required: + - name + ChatCompletionTool: + type: object + properties: + type: + type: string + enum: + - function + description: The type of the tool. Currently, only `function` is supported. + function: + $ref: '#/components/schemas/FunctionObject' + required: + - type + - function + FunctionObject: + type: object + properties: + description: + type: string + description: A description of what the function does, used by the model to choose when and how to call the function. + name: + type: string + description: The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + parameters: + $ref: '#/components/schemas/FunctionParameters' + strict: + type: boolean + nullable: true + default: false + description: Whether to enable strict schema adherence when generating the function call. If set to true, the model will follow the exact schema defined in the `parameters` field. Only a subset of JSON Schema is supported when `strict` is `true`. Learn more about Structured Outputs in the [function calling guide](docs/guides/function-calling). + required: + - name + ResponseFormatText: + type: object + properties: + type: + type: string + description: 'The type of response format being defined: `text`' + enum: + - text + required: + - type + ResponseFormatJsonObject: + type: object + properties: + type: + type: string + description: 'The type of response format being defined: `json_object`' + enum: + - json_object + required: + - type + ResponseFormatJsonSchemaSchema: + type: object + description: The schema for the response format, described as a JSON Schema object. + additionalProperties: true + ResponseFormatJsonSchema: + type: object + properties: + type: + type: string + description: 'The type of response format being defined: `json_schema`' + enum: + - json_schema + json_schema: + type: object + properties: + description: + type: string + description: A description of what the response format is for, used by the model to determine how to respond in the format. + name: + type: string + description: The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + schema: + $ref: '#/components/schemas/ResponseFormatJsonSchemaSchema' + strict: + type: boolean + nullable: true + default: false + description: Whether to enable strict schema adherence when generating the output. If set to true, the model will always follow the exact schema defined in the `schema` field. Only a subset of JSON Schema is supported when `strict` is `true`. To learn more, read the [Structured Outputs guide](/docs/guides/structured-outputs). + required: + - type + - name + required: + - type + - json_schema + ChatCompletionToolChoiceOption: + description: | + Controls which (if any) tool is called by the model. + `none` means the model will not call any tool and instead generates a message. + `auto` means the model can pick between generating a message or calling one or more tools. + `required` means the model must call one or more tools. + Specifying a particular tool via `{"type": "function", "function": {"name": "my_function"}}` forces the model to call that tool. + + `none` is the default when no tools are present. `auto` is the default if tools are present. + oneOf: + - type: string + description: | + `none` means the model will not call any tool and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools. + enum: + - none + - auto + - required + - $ref: '#/components/schemas/ChatCompletionNamedToolChoice' + x-oaiExpandable: true + ChatCompletionNamedToolChoice: + type: object + description: Specifies a tool the model should use. Use to force the model to call a specific function. + properties: + type: + type: string + enum: + - function + description: The type of the tool. Currently, only `function` is supported. + function: + type: object + properties: + name: + type: string + description: The name of the function to call. + required: + - name + required: + - type + - function + ParallelToolCalls: + description: Whether to enable [parallel function calling](/docs/guides/function-calling/parallel-function-calling) during tool use. + type: boolean + default: true + ChatCompletionMessageToolCalls: + type: array + description: The tool calls generated by the model, such as function calls. + items: + $ref: '#/components/schemas/ChatCompletionMessageToolCall' + ChatCompletionMessageToolCall: + type: object + properties: + id: + type: string + description: The ID of the tool call. + type: + type: string + enum: + - function + description: The type of the tool. Currently, only `function` is supported. + function: + type: object + description: The function that the model called. + properties: + name: + type: string + description: The name of the function to call. + arguments: + type: string + description: The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function. + required: + - name + - arguments + required: + - id + - type + - function + ChatCompletionMessageToolCallChunk: + type: object + properties: + index: + type: integer + id: + type: string + description: The ID of the tool call. + type: + type: string + enum: + - function + description: The type of the tool. Currently, only `function` is supported. + function: + type: object + properties: + name: + type: string + description: The name of the function to call. + arguments: + type: string + description: The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function. + required: + - index + ChatCompletionRole: + type: string + description: The role of the author of a message + enum: + - system + - user + - assistant + - tool + - function + ChatCompletionStreamOptions: + description: | + Options for streaming response. Only set this when you set `stream: true`. + type: object + nullable: true + default: {} + properties: + include_usage: + type: boolean + description: | + If set, an additional chunk will be streamed before the `data: [DONE]` message. The `usage` field on this chunk shows the token usage statistics for the entire request, and the `choices` field will always be an empty array. All other chunks will also include a `usage` field, but with a null value. + ChatCompletionResponseMessage: + type: object + description: A chat completion message generated by the model. + properties: + content: + type: string + description: The contents of the message. + nullable: true + refusal: + type: string + description: The refusal message generated by the model. + nullable: true + tool_calls: + $ref: '#/components/schemas/ChatCompletionMessageToolCalls' + role: + type: string + enum: + - assistant + description: The role of the author of this message. + function_call: + type: object + deprecated: true + description: Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + properties: + arguments: + type: string + description: The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function. + name: + type: string + description: The name of the function to call. + required: + - name + - arguments + required: + - role + - content + - refusal + ChatCompletionStreamResponseDelta: + type: object + description: A chat completion delta generated by streamed model responses. + properties: + content: + type: string + description: The contents of the chunk message. + nullable: true + function_call: + deprecated: true + type: object + description: Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + properties: + arguments: + type: string + description: The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function. + name: + type: string + description: The name of the function to call. + tool_calls: + type: array + items: + $ref: '#/components/schemas/ChatCompletionMessageToolCallChunk' + role: + type: string + enum: + - system + - user + - assistant + - tool + description: The role of the author of this message. + refusal: + type: string + description: The refusal message generated by the model. + nullable: true + CreateChatCompletionRequest: + type: object + properties: + messages: + description: A list of messages comprising the conversation so far. [Example Python code](https://cookbook.openai.com/examples/how_to_format_inputs_to_chatgpt_models). + type: array + minItems: 1 + items: + $ref: '#/components/schemas/ChatCompletionRequestMessage' + model: + description: ID of the model to use. See the [model endpoint compatibility](/docs/models/model-endpoint-compatibility) table for details on which models work with the Chat API. + example: gpt-4o + anyOf: + - type: string + - type: string + enum: + - gpt-4o + - gpt-4o-2024-05-13 + - gpt-4o-2024-08-06 + - gpt-4o-mini + - gpt-4o-mini-2024-07-18 + - gpt-4-turbo + - gpt-4-turbo-2024-04-09 + - gpt-4-0125-preview + - gpt-4-turbo-preview + - gpt-4-1106-preview + - gpt-4-vision-preview + - gpt-4 + - gpt-4-0314 + - gpt-4-0613 + - gpt-4-32k + - gpt-4-32k-0314 + - gpt-4-32k-0613 + - gpt-3.5-turbo + - gpt-3.5-turbo-16k + - gpt-3.5-turbo-0301 + - gpt-3.5-turbo-0613 + - gpt-3.5-turbo-1106 + - gpt-3.5-turbo-0125 + - gpt-3.5-turbo-16k-0613 + x-oaiTypeLabel: string + frequency_penalty: + type: number + default: 0 + minimum: -2 + maximum: 2 + nullable: true + description: | + Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. + + [See more information about frequency and presence penalties.](/docs/guides/text-generation/parameter-details) + logit_bias: + type: object + x-oaiTypeLabel: map + default: {} + nullable: true + additionalProperties: + type: integer + description: | + Modify the likelihood of specified tokens appearing in the completion. + + Accepts a JSON object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token. + logprobs: + description: Whether to return log probabilities of the output tokens or not. If true, returns the log probabilities of each output token returned in the `content` of `message`. + type: boolean + default: false + nullable: true + top_logprobs: + description: An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability. `logprobs` must be set to `true` if this parameter is used. + type: integer + minimum: 0 + maximum: 20 + nullable: true + max_tokens: + description: | + The maximum number of [tokens](/tokenizer) that can be generated in the chat completion. + + The total length of input tokens and generated tokens is limited by the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens. + type: integer + nullable: true + 'n': + type: integer + minimum: 1 + maximum: 128 + default: 1 + example: 1 + nullable: true + description: How many chat completion choices to generate for each input message. Note that you will be charged based on the number of generated tokens across all of the choices. Keep `n` as `1` to minimize costs. + presence_penalty: + type: number + default: 0 + minimum: -2 + maximum: 2 + nullable: true + description: | + Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. + + [See more information about frequency and presence penalties.](/docs/guides/text-generation/parameter-details) + response_format: + description: | + An object specifying the format that the model must output. Compatible with [GPT-4o](/docs/models/gpt-4o), [GPT-4o mini](/docs/models/gpt-4o-mini), [GPT-4 Turbo](/docs/models/gpt-4-and-gpt-4-turbo) and all GPT-3.5 Turbo models newer than `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured Outputs which guarantees the model will match your supplied JSON schema. Learn more in the [Structured Outputs guide](/docs/guides/structured-outputs). + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to produce JSON yourself via a system or user message. Without this, the model may generate an unending stream of whitespace until the generation reaches the token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be partially cut off if `finish_reason="length"`, which indicates the generation exceeded `max_tokens` or the conversation exceeded the max context length. + oneOf: + - $ref: '#/components/schemas/ResponseFormatText' + - $ref: '#/components/schemas/ResponseFormatJsonObject' + - $ref: '#/components/schemas/ResponseFormatJsonSchema' + x-oaiExpandable: true + seed: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + nullable: true + description: | + This feature is in Beta. + If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result. + Determinism is not guaranteed, and you should refer to the `system_fingerprint` response parameter to monitor changes in the backend. + x-oaiMeta: + beta: true + service_tier: + description: | + Specifies the latency tier to use for processing the request. This parameter is relevant for customers subscribed to the scale tier service: + - If set to 'auto', the system will utilize scale tier credits until they are exhausted. + - If set to 'default', the request will be processed using the default service tier with a lower uptime SLA and no latency guarentee. + - When not set, the default behavior is 'auto'. + + When this parameter is set, the response body will include the `service_tier` utilized. + type: string + enum: + - auto + - default + nullable: true + default: null + stop: + description: | + Up to 4 sequences where the API will stop generating further tokens. + default: [] + oneOf: + - type: string + nullable: true + - type: array + minItems: 1 + maxItems: 4 + items: + type: string + stream: + description: | + If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions). + type: boolean + nullable: true + default: false + stream_options: + $ref: '#/components/schemas/ChatCompletionStreamOptions' + temperature: + type: number + minimum: 0 + maximum: 2 + default: 1 + example: 1 + nullable: true + description: | + What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + top_p: + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + nullable: true + description: | + An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or `temperature` but not both. + tools: + type: array + description: | + A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported. + items: + $ref: '#/components/schemas/ChatCompletionTool' + tool_choice: + $ref: '#/components/schemas/ChatCompletionToolChoiceOption' + parallel_tool_calls: + $ref: '#/components/schemas/ParallelToolCalls' + user: + type: string + example: user-1234 + description: | + A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids). + function_call: + deprecated: true + description: | + Deprecated in favor of `tool_choice`. + + Controls which (if any) function is called by the model. + `none` means the model will not call a function and instead generates a message. + `auto` means the model can pick between generating a message or calling a function. + Specifying a particular function via `{"name": "my_function"}` forces the model to call that function. + + `none` is the default when no functions are present. `auto` is the default if functions are present. + oneOf: + - type: string + description: | + `none` means the model will not call a function and instead generates a message. `auto` means the model can pick between generating a message or calling a function. + enum: + - none + - auto + - $ref: '#/components/schemas/ChatCompletionFunctionCallOption' + x-oaiExpandable: true + functions: + deprecated: true + description: | + Deprecated in favor of `tools`. + + A list of functions the model may generate JSON inputs for. + type: array + minItems: 1 + maxItems: 128 + items: + $ref: '#/components/schemas/ChatCompletionFunctions' + required: + - model + - messages + CreateChatCompletionResponse: + type: object + description: Represents a chat completion response returned by model, based on the provided input. + properties: + id: + type: string + description: A unique identifier for the chat completion. + choices: + type: array + description: A list of chat completion choices. Can be more than one if `n` is greater than 1. + items: + type: object + required: + - finish_reason + - index + - message + - logprobs + properties: + finish_reason: + type: string + description: | + The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence, + `length` if the maximum number of tokens specified in the request was reached, + `content_filter` if content was omitted due to a flag from our content filters, + `tool_calls` if the model called a tool, or `function_call` (deprecated) if the model called a function. + enum: + - stop + - length + - tool_calls + - content_filter + - function_call + index: + type: integer + description: The index of the choice in the list of choices. + message: + $ref: '#/components/schemas/ChatCompletionResponseMessage' + logprobs: + description: Log probability information for the choice. + type: object + nullable: true + properties: + content: + description: A list of message content tokens with log probability information. + type: array + items: + $ref: '#/components/schemas/ChatCompletionTokenLogprob' + nullable: true + refusal: + description: A list of message refusal tokens with log probability information. + type: array + items: + $ref: '#/components/schemas/ChatCompletionTokenLogprob' + nullable: true + required: + - content + - refusal + created: + type: integer + description: The Unix timestamp (in seconds) of when the chat completion was created. + model: + type: string + description: The model used for the chat completion. + service_tier: + description: The service tier used for processing the request. This field is only included if the `service_tier` parameter is specified in the request. + type: string + enum: + - scale + - default + example: scale + nullable: true + system_fingerprint: + type: string + description: | + This fingerprint represents the backend configuration that the model runs with. + + Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. + object: + type: string + description: The object type, which is always `chat.completion`. + enum: + - chat.completion + usage: + $ref: '#/components/schemas/CompletionUsage' + required: + - choices + - created + - id + - model + - object + x-oaiMeta: + name: The chat completion object + group: chat + example: | + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4o-mini", + "system_fingerprint": "fp_44709d6fcb", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "\n\nHello there, how may I assist you today?", + }, + "logprobs": null, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + } + CreateChatCompletionFunctionResponse: + type: object + description: Represents a chat completion response returned by model, based on the provided input. + properties: + id: + type: string + description: A unique identifier for the chat completion. + choices: + type: array + description: A list of chat completion choices. Can be more than one if `n` is greater than 1. + items: + type: object + required: + - finish_reason + - index + - message + - logprobs + properties: + finish_reason: + type: string + description: | + The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence, `length` if the maximum number of tokens specified in the request was reached, `content_filter` if content was omitted due to a flag from our content filters, or `function_call` if the model called a function. + enum: + - stop + - length + - function_call + - content_filter + index: + type: integer + description: The index of the choice in the list of choices. + message: + $ref: '#/components/schemas/ChatCompletionResponseMessage' + created: + type: integer + description: The Unix timestamp (in seconds) of when the chat completion was created. + model: + type: string + description: The model used for the chat completion. + system_fingerprint: + type: string + description: | + This fingerprint represents the backend configuration that the model runs with. + + Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. + object: + type: string + description: The object type, which is always `chat.completion`. + enum: + - chat.completion + usage: + $ref: '#/components/schemas/CompletionUsage' + required: + - choices + - created + - id + - model + - object + x-oaiMeta: + name: The chat completion object + group: chat + example: | + { + "id": "chatcmpl-abc123", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_abc123", + "type": "function", + "function": { + "name": "get_current_weather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } + } + ChatCompletionTokenLogprob: + type: object + properties: + token: + description: The token. + type: string + logprob: + description: The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely. + type: number + bytes: + description: A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token. + type: array + items: + type: integer + nullable: true + top_logprobs: + description: List of the most likely tokens and their log probability, at this token position. In rare cases, there may be fewer than the number of requested `top_logprobs` returned. + type: array + items: + type: object + properties: + token: + description: The token. + type: string + logprob: + description: The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value `-9999.0` is used to signify that the token is very unlikely. + type: number + bytes: + description: A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by multiple tokens and their byte representations must be combined to generate the correct text representation. Can be `null` if there is no bytes representation for the token. + type: array + items: + type: integer + nullable: true + required: + - token + - logprob + - bytes + required: + - token + - logprob + - bytes + - top_logprobs + ListPaginatedFineTuningJobsResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/FineTuningJob' + has_more: + type: boolean + object: + type: string + enum: + - list + required: + - object + - data + - has_more + CreateChatCompletionStreamResponse: + type: object + description: Represents a streamed chunk of a chat completion response returned by model, based on the provided input. + properties: + id: + type: string + description: A unique identifier for the chat completion. Each chunk has the same ID. + choices: + type: array + description: | + A list of chat completion choices. Can contain more than one elements if `n` is greater than 1. Can also be empty for the + last chunk if you set `stream_options: {"include_usage": true}`. + items: + type: object + required: + - delta + - finish_reason + - index + properties: + delta: + $ref: '#/components/schemas/ChatCompletionStreamResponseDelta' + logprobs: + description: Log probability information for the choice. + type: object + nullable: true + properties: + content: + description: A list of message content tokens with log probability information. + type: array + items: + $ref: '#/components/schemas/ChatCompletionTokenLogprob' + nullable: true + refusal: + description: A list of message refusal tokens with log probability information. + type: array + items: + $ref: '#/components/schemas/ChatCompletionTokenLogprob' + nullable: true + required: + - content + - refusal + finish_reason: + type: string + description: | + The reason the model stopped generating tokens. This will be `stop` if the model hit a natural stop point or a provided stop sequence, + `length` if the maximum number of tokens specified in the request was reached, + `content_filter` if content was omitted due to a flag from our content filters, + `tool_calls` if the model called a tool, or `function_call` (deprecated) if the model called a function. + enum: + - stop + - length + - tool_calls + - content_filter + - function_call + nullable: true + index: + type: integer + description: The index of the choice in the list of choices. + created: + type: integer + description: The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same timestamp. + model: + type: string + description: The model to generate the completion. + service_tier: + description: The service tier used for processing the request. This field is only included if the `service_tier` parameter is specified in the request. + type: string + enum: + - scale + - default + example: scale + nullable: true + system_fingerprint: + type: string + description: | + This fingerprint represents the backend configuration that the model runs with. + Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. + object: + type: string + description: The object type, which is always `chat.completion.chunk`. + enum: + - chat.completion.chunk + usage: + type: object + description: | + An optional field that will only be present when you set `stream_options: {"include_usage": true}` in your request. + When present, it contains a null value except for the last chunk which contains the token usage statistics for the entire request. + properties: + completion_tokens: + type: integer + description: Number of tokens in the generated completion. + prompt_tokens: + type: integer + description: Number of tokens in the prompt. + total_tokens: + type: integer + description: Total number of tokens used in the request (prompt + completion). + required: + - prompt_tokens + - completion_tokens + - total_tokens + required: + - choices + - created + - id + - model + - object + x-oaiMeta: + name: The chat completion chunk object + group: chat + example: | + {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]} + + {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null}]} + + .... + + {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o-mini", "system_fingerprint": "fp_44709d6fcb", "choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + CreateChatCompletionImageResponse: + type: object + description: Represents a streamed chunk of a chat completion response returned by model, based on the provided input. + x-oaiMeta: + name: The chat completion chunk object + group: chat + example: | + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4o-mini", + "system_fingerprint": "fp_44709d6fcb", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "\n\nThis image shows a wooden boardwalk extending through a lush green marshland.", + }, + "logprobs": null, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + } + CreateImageRequest: + type: object + properties: + prompt: + description: A text description of the desired image(s). The maximum length is 1000 characters for `dall-e-2` and 4000 characters for `dall-e-3`. + type: string + example: A cute baby sea otter + model: + anyOf: + - type: string + - type: string + enum: + - dall-e-2 + - dall-e-3 + x-oaiTypeLabel: string + default: dall-e-2 + example: dall-e-3 + nullable: true + description: The model to use for image generation. + 'n': + type: integer + minimum: 1 + maximum: 10 + default: 1 + example: 1 + nullable: true + description: The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only `n=1` is supported. + quality: + type: string + enum: + - standard + - hd + default: standard + example: standard + description: The quality of the image that will be generated. `hd` creates images with finer details and greater consistency across the image. This param is only supported for `dall-e-3`. + response_format: + type: string + enum: + - url + - b64_json + default: url + example: url + nullable: true + description: The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. + size: + type: string + enum: + - 256x256 + - 512x512 + - 1024x1024 + - 1792x1024 + - 1024x1792 + default: 1024x1024 + example: 1024x1024 + nullable: true + description: The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024` for `dall-e-2`. Must be one of `1024x1024`, `1792x1024`, or `1024x1792` for `dall-e-3` models. + style: + type: string + enum: + - vivid + - natural + default: vivid + example: vivid + nullable: true + description: The style of the generated images. Must be one of `vivid` or `natural`. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for `dall-e-3`. + user: + type: string + example: user-1234 + description: | + A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids). + required: + - prompt + ImagesResponse: + properties: + created: + type: integer + data: + type: array + items: + $ref: '#/components/schemas/Image' + required: + - created + - data + Image: + type: object + description: Represents the url or the content of an image generated by the OpenAI API. + properties: + b64_json: + type: string + description: The base64-encoded JSON of the generated image, if `response_format` is `b64_json`. + url: + type: string + description: The URL of the generated image, if `response_format` is `url` (default). + revised_prompt: + type: string + description: The prompt that was used to generate the image, if there was any revision to the prompt. + x-oaiMeta: + name: The image object + example: | + { + "url": "...", + "revised_prompt": "..." + } + CreateImageEditRequest: + type: object + properties: + image: + description: The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not provided, image must have transparency, which will be used as the mask. + type: string + format: binary + prompt: + description: A text description of the desired image(s). The maximum length is 1000 characters. + type: string + example: A cute baby sea otter wearing a beret + mask: + description: An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where `image` should be edited. Must be a valid PNG file, less than 4MB, and have the same dimensions as `image`. + type: string + format: binary + model: + anyOf: + - type: string + - type: string + enum: + - dall-e-2 + x-oaiTypeLabel: string + default: dall-e-2 + example: dall-e-2 + nullable: true + description: The model to use for image generation. Only `dall-e-2` is supported at this time. + 'n': + type: integer + minimum: 1 + maximum: 10 + default: 1 + example: 1 + nullable: true + description: The number of images to generate. Must be between 1 and 10. + size: + type: string + enum: + - 256x256 + - 512x512 + - 1024x1024 + default: 1024x1024 + example: 1024x1024 + nullable: true + description: The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`. + response_format: + type: string + enum: + - url + - b64_json + default: url + example: url + nullable: true + description: The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. + user: + type: string + example: user-1234 + description: | + A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids). + required: + - prompt + - image + CreateImageVariationRequest: + type: object + properties: + image: + description: The image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, and square. + type: string + format: binary + model: + anyOf: + - type: string + - type: string + enum: + - dall-e-2 + x-oaiTypeLabel: string + default: dall-e-2 + example: dall-e-2 + nullable: true + description: The model to use for image generation. Only `dall-e-2` is supported at this time. + 'n': + type: integer + minimum: 1 + maximum: 10 + default: 1 + example: 1 + nullable: true + description: The number of images to generate. Must be between 1 and 10. For `dall-e-3`, only `n=1` is supported. + response_format: + type: string + enum: + - url + - b64_json + default: url + example: url + nullable: true + description: The format in which the generated images are returned. Must be one of `url` or `b64_json`. URLs are only valid for 60 minutes after the image has been generated. + size: + type: string + enum: + - 256x256 + - 512x512 + - 1024x1024 + default: 1024x1024 + example: 1024x1024 + nullable: true + description: The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`. + user: + type: string + example: user-1234 + description: | + A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids). + required: + - image + CreateModerationRequest: + type: object + properties: + input: + description: The input text to classify + oneOf: + - type: string + default: '' + example: I want to kill them. + - type: array + items: + type: string + default: '' + example: I want to kill them. + model: + description: | + Two content moderations models are available: `text-moderation-stable` and `text-moderation-latest`. + + The default is `text-moderation-latest` which will be automatically upgraded over time. This ensures you are always using our most accurate model. If you use `text-moderation-stable`, we will provide advanced notice before updating the model. Accuracy of `text-moderation-stable` may be slightly lower than for `text-moderation-latest`. + nullable: false + default: text-moderation-latest + example: text-moderation-stable + anyOf: + - type: string + - type: string + enum: + - text-moderation-latest + - text-moderation-stable + x-oaiTypeLabel: string + required: + - input + CreateModerationResponse: + type: object + description: Represents if a given text input is potentially harmful. + properties: + id: + type: string + description: The unique identifier for the moderation request. + model: + type: string + description: The model used to generate the moderation results. + results: + type: array + description: A list of moderation objects. + items: + type: object + properties: + flagged: + type: boolean + description: Whether any of the below categories are flagged. + categories: + type: object + description: A list of the categories, and whether they are flagged or not. + properties: + hate: + type: boolean + description: Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment. + hate/threatening: + type: boolean + description: Hateful content that also includes violence or serious harm towards the targeted group based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. + harassment: + type: boolean + description: Content that expresses, incites, or promotes harassing language towards any target. + harassment/threatening: + type: boolean + description: Harassment content that also includes violence or serious harm towards any target. + self-harm: + type: boolean + description: Content that promotes, encourages, or depicts acts of self-harm, such as suicide, cutting, and eating disorders. + self-harm/intent: + type: boolean + description: Content where the speaker expresses that they are engaging or intend to engage in acts of self-harm, such as suicide, cutting, and eating disorders. + self-harm/instructions: + type: boolean + description: Content that encourages performing acts of self-harm, such as suicide, cutting, and eating disorders, or that gives instructions or advice on how to commit such acts. + sexual: + type: boolean + description: Content meant to arouse sexual excitement, such as the description of sexual activity, or that promotes sexual services (excluding sex education and wellness). + sexual/minors: + type: boolean + description: Sexual content that includes an individual who is under 18 years old. + violence: + type: boolean + description: Content that depicts death, violence, or physical injury. + violence/graphic: + type: boolean + description: Content that depicts death, violence, or physical injury in graphic detail. + required: + - hate + - hate/threatening + - harassment + - harassment/threatening + - self-harm + - self-harm/intent + - self-harm/instructions + - sexual + - sexual/minors + - violence + - violence/graphic + category_scores: + type: object + description: A list of the categories along with their scores as predicted by model. + properties: + hate: + type: number + description: The score for the category 'hate'. + hate/threatening: + type: number + description: The score for the category 'hate/threatening'. + harassment: + type: number + description: The score for the category 'harassment'. + harassment/threatening: + type: number + description: The score for the category 'harassment/threatening'. + self-harm: + type: number + description: The score for the category 'self-harm'. + self-harm/intent: + type: number + description: The score for the category 'self-harm/intent'. + self-harm/instructions: + type: number + description: The score for the category 'self-harm/instructions'. + sexual: + type: number + description: The score for the category 'sexual'. + sexual/minors: + type: number + description: The score for the category 'sexual/minors'. + violence: + type: number + description: The score for the category 'violence'. + violence/graphic: + type: number + description: The score for the category 'violence/graphic'. + required: + - hate + - hate/threatening + - harassment + - harassment/threatening + - self-harm + - self-harm/intent + - self-harm/instructions + - sexual + - sexual/minors + - violence + - violence/graphic + required: + - flagged + - categories + - category_scores + required: + - id + - model + - results + x-oaiMeta: + name: The moderation object + example: | + { + "id": "modr-XXXXX", + "model": "text-moderation-005", + "results": [ + { + "flagged": true, + "categories": { + "sexual": false, + "hate": false, + "harassment": false, + "self-harm": false, + "sexual/minors": false, + "hate/threatening": false, + "violence/graphic": false, + "self-harm/intent": false, + "self-harm/instructions": false, + "harassment/threatening": true, + "violence": true, + }, + "category_scores": { + "sexual": 1.2282071e-06, + "hate": 0.010696256, + "harassment": 0.29842457, + "self-harm": 1.5236925e-08, + "sexual/minors": 5.7246268e-08, + "hate/threatening": 0.0060676364, + "violence/graphic": 4.435014e-06, + "self-harm/intent": 8.098441e-10, + "self-harm/instructions": 2.8498655e-11, + "harassment/threatening": 0.63055265, + "violence": 0.99011886, + } + } + ] + } + ListFilesResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/OpenAIFile' + object: + type: string + enum: + - list + required: + - object + - data + CreateFileRequest: + type: object + additionalProperties: false + properties: + file: + description: | + The File object (not file name) to be uploaded. + type: string + format: binary + purpose: + description: | + The intended purpose of the uploaded file. + + Use "assistants" for [Assistants](/docs/api-reference/assistants) and [Message](/docs/api-reference/messages) files, "vision" for Assistants image file inputs, "batch" for [Batch API](/docs/guides/batch), and "fine-tune" for [Fine-tuning](/docs/api-reference/fine-tuning). + type: string + enum: + - assistants + - batch + - fine-tune + - vision + required: + - file + - purpose + DeleteFileResponse: + type: object + properties: + id: + type: string + object: + type: string + enum: + - file + deleted: + type: boolean + required: + - id + - object + - deleted + CreateUploadRequest: + type: object + additionalProperties: false + properties: + filename: + description: | + The name of the file to upload. + type: string + purpose: + description: | + The intended purpose of the uploaded file. + + See the [documentation on File purposes](/docs/api-reference/files/create#files-create-purpose). + type: string + enum: + - assistants + - batch + - fine-tune + - vision + bytes: + description: | + The number of bytes in the file you are uploading. + type: integer + mime_type: + description: | + The MIME type of the file. + + This must fall within the supported MIME types for your file purpose. See the supported MIME types for assistants and vision. + type: string + required: + - filename + - purpose + - bytes + - mime_type + AddUploadPartRequest: + type: object + additionalProperties: false + properties: + data: + description: | + The chunk of bytes for this Part. + type: string + format: binary + required: + - data + CompleteUploadRequest: + type: object + additionalProperties: false + properties: + part_ids: + type: array + description: | + The ordered list of Part IDs. + items: + type: string + md5: + description: | + The optional md5 checksum for the file contents to verify if the bytes uploaded matches what you expect. + type: string + required: + - part_ids + CancelUploadRequest: + type: object + additionalProperties: false + CreateFineTuningJobRequest: + type: object + properties: + model: + description: | + The name of the model to fine-tune. You can select one of the + [supported models](/docs/guides/fine-tuning/which-models-can-be-fine-tuned). + example: gpt-4o-mini + anyOf: + - type: string + - type: string + enum: + - babbage-002 + - davinci-002 + - gpt-3.5-turbo + - gpt-4o-mini + x-oaiTypeLabel: string + training_file: + description: | + The ID of an uploaded file that contains training data. + + See [upload file](/docs/api-reference/files/create) for how to upload a file. + + Your dataset must be formatted as a JSONL file. Additionally, you must upload your file with the purpose `fine-tune`. + + The contents of the file should differ depending on if the model uses the [chat](/docs/api-reference/fine-tuning/chat-input) or [completions](/docs/api-reference/fine-tuning/completions-input) format. + + See the [fine-tuning guide](/docs/guides/fine-tuning) for more details. + type: string + example: file-abc123 + hyperparameters: + type: object + description: The hyperparameters used for the fine-tuning job. + properties: + batch_size: + description: | + Number of examples in each batch. A larger batch size means that model parameters + are updated less frequently, but with lower variance. + oneOf: + - type: string + enum: + - auto + - type: integer + minimum: 1 + maximum: 256 + default: auto + learning_rate_multiplier: + description: | + Scaling factor for the learning rate. A smaller learning rate may be useful to avoid + overfitting. + oneOf: + - type: string + enum: + - auto + - type: number + minimum: 0 + exclusiveMinimum: true + default: auto + n_epochs: + description: | + The number of epochs to train the model for. An epoch refers to one full cycle + through the training dataset. + oneOf: + - type: string + enum: + - auto + - type: integer + minimum: 1 + maximum: 50 + default: auto + suffix: + description: | + A string of up to 18 characters that will be added to your fine-tuned model name. + + For example, a `suffix` of "custom-model-name" would produce a model name like `ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`. + type: string + minLength: 1 + maxLength: 40 + default: null + nullable: true + validation_file: + description: | + The ID of an uploaded file that contains validation data. + + If you provide this file, the data is used to generate validation + metrics periodically during fine-tuning. These metrics can be viewed in + the fine-tuning results file. + The same data should not be present in both train and validation files. + + Your dataset must be formatted as a JSONL file. You must upload your file with the purpose `fine-tune`. + + See the [fine-tuning guide](/docs/guides/fine-tuning) for more details. + type: string + nullable: true + example: file-abc123 + integrations: + type: array + description: A list of integrations to enable for your fine-tuning job. + nullable: true + items: + type: object + required: + - type + - wandb + properties: + type: + description: | + The type of integration to enable. Currently, only "wandb" (Weights and Biases) is supported. + oneOf: + - type: string + enum: + - wandb + wandb: + type: object + description: | + The settings for your integration with Weights and Biases. This payload specifies the project that + metrics will be sent to. Optionally, you can set an explicit display name for your run, add tags + to your run, and set a default entity (team, username, etc) to be associated with your run. + required: + - project + properties: + project: + description: | + The name of the project that the new run will be created under. + type: string + example: my-wandb-project + name: + description: | + A display name to set for the run. If not set, we will use the Job ID as the name. + nullable: true + type: string + entity: + description: | + The entity to use for the run. This allows you to set the team or username of the WandB user that you would + like associated with the run. If not set, the default entity for the registered WandB API key is used. + nullable: true + type: string + tags: + description: | + A list of tags to be attached to the newly created run. These tags are passed through directly to WandB. Some + default tags are generated by OpenAI: "openai/finetune", "openai/{base-model}", "openai/{ftjob-abcdef}". + type: array + items: + type: string + example: custom-tag + seed: + description: | + The seed controls the reproducibility of the job. Passing in the same seed and job parameters should produce the same results, but may differ in rare cases. + If a seed is not specified, one will be generated for you. + type: integer + nullable: true + minimum: 0 + maximum: 2147483647 + example: 42 + required: + - model + - training_file + ListFineTuningJobEventsResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/FineTuningJobEvent' + object: + type: string + enum: + - list + required: + - object + - data + ListFineTuningJobCheckpointsResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/FineTuningJobCheckpoint' + object: + type: string + enum: + - list + first_id: + type: string + nullable: true + last_id: + type: string + nullable: true + has_more: + type: boolean + required: + - object + - data + - has_more + CreateEmbeddingRequest: + type: object + additionalProperties: false + properties: + input: + description: | + Input text to embed, encoded as a string or array of tokens. To embed multiple inputs in a single request, pass an array of strings or array of token arrays. The input must not exceed the max input tokens for the model (8192 tokens for `text-embedding-ada-002`), cannot be an empty string, and any array must be 2048 dimensions or less. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens. + example: The quick brown fox jumped over the lazy dog + oneOf: + - type: string + title: string + description: The string that will be turned into an embedding. + default: '' + example: This is a test. + - type: array + title: array + description: The array of strings that will be turned into an embedding. + minItems: 1 + maxItems: 2048 + items: + type: string + default: '' + example: '[''This is a test.'']' + - type: array + title: array + description: The array of integers that will be turned into an embedding. + minItems: 1 + maxItems: 2048 + items: + type: integer + example: '[1212, 318, 257, 1332, 13]' + - type: array + title: array + description: The array of arrays containing integers that will be turned into an embedding. + minItems: 1 + maxItems: 2048 + items: + type: array + minItems: 1 + items: + type: integer + example: '[[1212, 318, 257, 1332, 13]]' + x-oaiExpandable: true + model: + description: | + ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models/overview) for descriptions of them. + example: text-embedding-3-small + anyOf: + - type: string + - type: string + enum: + - text-embedding-ada-002 + - text-embedding-3-small + - text-embedding-3-large + x-oaiTypeLabel: string + encoding_format: + description: The format to return the embeddings in. Can be either `float` or [`base64`](https://pypi.org/project/pybase64/). + example: float + default: float + type: string + enum: + - float + - base64 + dimensions: + description: | + The number of dimensions the resulting output embeddings should have. Only supported in `text-embedding-3` and later models. + type: integer + minimum: 1 + user: + type: string + example: user-1234 + description: | + A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids). + required: + - model + - input + CreateEmbeddingResponse: + type: object + properties: + data: + type: array + description: The list of embeddings generated by the model. + items: + $ref: '#/components/schemas/Embedding' + model: + type: string + description: The name of the model used to generate the embedding. + object: + type: string + description: The object type, which is always "list". + enum: + - list + usage: + type: object + description: The usage information for the request. + properties: + prompt_tokens: + type: integer + description: The number of tokens used by the prompt. + total_tokens: + type: integer + description: The total number of tokens used by the request. + required: + - prompt_tokens + - total_tokens + required: + - object + - model + - data + - usage + CreateTranscriptionRequest: + type: object + additionalProperties: false + properties: + file: + description: | + The audio file object (not file name) to transcribe, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. + type: string + x-oaiTypeLabel: file + format: binary + model: + description: | + ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper V2 model) is currently available. + example: whisper-1 + anyOf: + - type: string + - type: string + enum: + - whisper-1 + x-oaiTypeLabel: string + language: + description: | + The language of the input audio. Supplying the input language in [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format will improve accuracy and latency. + type: string + prompt: + description: | + An optional text to guide the model's style or continue a previous audio segment. The [prompt](/docs/guides/speech-to-text/prompting) should match the audio language. + type: string + response_format: + description: | + The format of the transcript output, in one of these options: `json`, `text`, `srt`, `verbose_json`, or `vtt`. + type: string + enum: + - json + - text + - srt + - verbose_json + - vtt + default: json + temperature: + description: | + The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use [log probability](https://en.wikipedia.org/wiki/Log_probability) to automatically increase the temperature until certain thresholds are hit. + type: number + default: 0 + timestamp_granularities[]: + description: | + The timestamp granularities to populate for this transcription. `response_format` must be set `verbose_json` to use timestamp granularities. Either or both of these options are supported: `word`, or `segment`. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency. + type: array + items: + type: string + enum: + - word + - segment + default: + - segment + required: + - file + - model + CreateTranscriptionResponseJson: + type: object + description: Represents a transcription response returned by model, based on the provided input. + properties: + text: + type: string + description: The transcribed text. + required: + - text + x-oaiMeta: + name: The transcription object (JSON) + group: audio + example: | + { + "text": "Imagine the wildest idea that you've ever had, and you're curious about how it might scale to something that's a 100, a 1,000 times bigger. This is a place where you can get to do that." + } + TranscriptionSegment: + type: object + properties: + id: + type: integer + description: Unique identifier of the segment. + seek: + type: integer + description: Seek offset of the segment. + start: + type: number + format: float + description: Start time of the segment in seconds. + end: + type: number + format: float + description: End time of the segment in seconds. + text: + type: string + description: Text content of the segment. + tokens: + type: array + items: + type: integer + description: Array of token IDs for the text content. + temperature: + type: number + format: float + description: Temperature parameter used for generating the segment. + avg_logprob: + type: number + format: float + description: Average logprob of the segment. If the value is lower than -1, consider the logprobs failed. + compression_ratio: + type: number + format: float + description: Compression ratio of the segment. If the value is greater than 2.4, consider the compression failed. + no_speech_prob: + type: number + format: float + description: Probability of no speech in the segment. If the value is higher than 1.0 and the `avg_logprob` is below -1, consider this segment silent. + required: + - id + - seek + - start + - end + - text + - tokens + - temperature + - avg_logprob + - compression_ratio + - no_speech_prob + TranscriptionWord: + type: object + properties: + word: + type: string + description: The text content of the word. + start: + type: number + format: float + description: Start time of the word in seconds. + end: + type: number + format: float + description: End time of the word in seconds. + required: + - word + - start + - end + CreateTranscriptionResponseVerboseJson: + type: object + description: Represents a verbose json transcription response returned by model, based on the provided input. + properties: + language: + type: string + description: The language of the input audio. + duration: + type: string + description: The duration of the input audio. + text: + type: string + description: The transcribed text. + words: + type: array + description: Extracted words and their corresponding timestamps. + items: + $ref: '#/components/schemas/TranscriptionWord' + segments: + type: array + description: Segments of the transcribed text and their corresponding details. + items: + $ref: '#/components/schemas/TranscriptionSegment' + required: + - language + - duration + - text + x-oaiMeta: + name: The transcription object (Verbose JSON) + group: audio + example: | + { + "task": "transcribe", + "language": "english", + "duration": 8.470000267028809, + "text": "The beach was a popular spot on a hot summer day. People were swimming in the ocean, building sandcastles, and playing beach volleyball.", + "segments": [ + { + "id": 0, + "seek": 0, + "start": 0.0, + "end": 3.319999933242798, + "text": " The beach was a popular spot on a hot summer day.", + "tokens": [ + 50364, 440, 7534, 390, 257, 3743, 4008, 322, 257, 2368, 4266, 786, 13, 50530 + ], + "temperature": 0.0, + "avg_logprob": -0.2860786020755768, + "compression_ratio": 1.2363636493682861, + "no_speech_prob": 0.00985979475080967 + }, + ... + ] + } + CreateTranslationRequest: + type: object + additionalProperties: false + properties: + file: + description: | + The audio file object (not file name) translate, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm. + type: string + x-oaiTypeLabel: file + format: binary + model: + description: | + ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper V2 model) is currently available. + example: whisper-1 + anyOf: + - type: string + - type: string + enum: + - whisper-1 + x-oaiTypeLabel: string + prompt: + description: | + An optional text to guide the model's style or continue a previous audio segment. The [prompt](/docs/guides/speech-to-text/prompting) should be in English. + type: string + response_format: + description: | + The format of the transcript output, in one of these options: `json`, `text`, `srt`, `verbose_json`, or `vtt`. + type: string + default: json + temperature: + description: | + The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use [log probability](https://en.wikipedia.org/wiki/Log_probability) to automatically increase the temperature until certain thresholds are hit. + type: number + default: 0 + required: + - file + - model + CreateTranslationResponseJson: + type: object + properties: + text: + type: string + required: + - text + CreateTranslationResponseVerboseJson: + type: object + properties: + language: + type: string + description: The language of the output translation (always `english`). + duration: + type: string + description: The duration of the input audio. + text: + type: string + description: The translated text. + segments: + type: array + description: Segments of the translated text and their corresponding details. + items: + $ref: '#/components/schemas/TranscriptionSegment' + required: + - language + - duration + - text + CreateSpeechRequest: + type: object + additionalProperties: false + properties: + model: + description: | + One of the available [TTS models](/docs/models/tts): `tts-1` or `tts-1-hd` + anyOf: + - type: string + - type: string + enum: + - tts-1 + - tts-1-hd + x-oaiTypeLabel: string + input: + type: string + description: The text to generate audio for. The maximum length is 4096 characters. + maxLength: 4096 + voice: + description: The voice to use when generating the audio. Supported voices are `alloy`, `echo`, `fable`, `onyx`, `nova`, and `shimmer`. Previews of the voices are available in the [Text to speech guide](/docs/guides/text-to-speech/voice-options). + type: string + enum: + - alloy + - echo + - fable + - onyx + - nova + - shimmer + response_format: + description: The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, `wav`, and `pcm`. + default: mp3 + type: string + enum: + - mp3 + - opus + - aac + - flac + - wav + - pcm + speed: + description: The speed of the generated audio. Select a value from `0.25` to `4.0`. `1.0` is the default. + type: number + default: 1 + minimum: 0.25 + maximum: 4 + required: + - model + - input + - voice + Model: + title: Model + description: Describes an OpenAI model offering that can be used with the API. + properties: + id: + type: string + description: The model identifier, which can be referenced in the API endpoints. + created: + type: integer + description: The Unix timestamp (in seconds) when the model was created. + object: + type: string + description: The object type, which is always "model". + enum: + - model + owned_by: + type: string + description: The organization that owns the model. + required: + - id + - object + - created + - owned_by + x-oaiMeta: + name: The model object + example: | + { + "id": "VAR_model_id", + "object": "model", + "created": 1686935002, + "owned_by": "openai" + } + OpenAIFile: + title: OpenAIFile + description: The `File` object represents a document that has been uploaded to OpenAI. + properties: + id: + type: string + description: The file identifier, which can be referenced in the API endpoints. + bytes: + type: integer + description: The size of the file, in bytes. + created_at: + type: integer + description: The Unix timestamp (in seconds) for when the file was created. + filename: + type: string + description: The name of the file. + object: + type: string + description: The object type, which is always `file`. + enum: + - file + purpose: + type: string + description: The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results` and `vision`. + enum: + - assistants + - assistants_output + - batch + - batch_output + - fine-tune + - fine-tune-results + - vision + status: + type: string + deprecated: true + description: Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`. + enum: + - uploaded + - processed + - error + status_details: + type: string + deprecated: true + description: Deprecated. For details on why a fine-tuning training file failed validation, see the `error` field on `fine_tuning.job`. + required: + - id + - object + - bytes + - created_at + - filename + - purpose + - status + x-oaiMeta: + name: The file object + example: | + { + "id": "file-abc123", + "object": "file", + "bytes": 120000, + "created_at": 1677610602, + "filename": "salesOverview.pdf", + "purpose": "assistants", + } + Upload: + type: object + title: Upload + description: | + The Upload object can accept byte chunks in the form of Parts. + properties: + id: + type: string + description: The Upload unique identifier, which can be referenced in API endpoints. + created_at: + type: integer + description: The Unix timestamp (in seconds) for when the Upload was created. + filename: + type: string + description: The name of the file to be uploaded. + bytes: + type: integer + description: The intended number of bytes to be uploaded. + purpose: + type: string + description: The intended purpose of the file. [Please refer here](/docs/api-reference/files/object#files/object-purpose) for acceptable values. + status: + type: string + description: The status of the Upload. + enum: + - pending + - completed + - cancelled + - expired + expires_at: + type: integer + description: The Unix timestamp (in seconds) for when the Upload was created. + object: + type: string + description: The object type, which is always "upload". + enum: + - upload + file: + $ref: '#/components/schemas/OpenAIFile' + nullable: true + description: The ready File object after the Upload is completed. + required: + - bytes + - created_at + - expires_at + - filename + - id + - purpose + - status + - step_number + x-oaiMeta: + name: The upload object + example: | + { + "id": "upload_abc123", + "object": "upload", + "bytes": 2147483648, + "created_at": 1719184911, + "filename": "training_examples.jsonl", + "purpose": "fine-tune", + "status": "completed", + "expires_at": 1719127296, + "file": { + "id": "file-xyz321", + "object": "file", + "bytes": 2147483648, + "created_at": 1719186911, + "filename": "training_examples.jsonl", + "purpose": "fine-tune", + } + } + UploadPart: + type: object + title: UploadPart + description: | + The upload Part represents a chunk of bytes we can add to an Upload object. + properties: + id: + type: string + description: The upload Part unique identifier, which can be referenced in API endpoints. + created_at: + type: integer + description: The Unix timestamp (in seconds) for when the Part was created. + upload_id: + type: string + description: The ID of the Upload object that this Part was added to. + object: + type: string + description: The object type, which is always `upload.part`. + enum: + - upload.part + required: + - created_at + - id + - object + - upload_id + x-oaiMeta: + name: The upload part object + example: | + { + "id": "part_def456", + "object": "upload.part", + "created_at": 1719186911, + "upload_id": "upload_abc123" + } + Embedding: + type: object + description: | + Represents an embedding vector returned by embedding endpoint. + properties: + index: + type: integer + description: The index of the embedding in the list of embeddings. + embedding: + type: array + description: | + The embedding vector, which is a list of floats. The length of vector depends on the model as listed in the [embedding guide](/docs/guides/embeddings). + items: + type: number + object: + type: string + description: The object type, which is always "embedding". + enum: + - embedding + required: + - index + - object + - embedding + x-oaiMeta: + name: The embedding object + example: | + { + "object": "embedding", + "embedding": [ + 0.0023064255, + -0.009327292, + .... (1536 floats total for ada-002) + -0.0028842222, + ], + "index": 0 + } + FineTuningJob: + type: object + title: FineTuningJob + description: | + The `fine_tuning.job` object represents a fine-tuning job that has been created through the API. + properties: + id: + type: string + description: The object identifier, which can be referenced in the API endpoints. + created_at: + type: integer + description: The Unix timestamp (in seconds) for when the fine-tuning job was created. + error: + type: object + nullable: true + description: For fine-tuning jobs that have `failed`, this will contain more information on the cause of the failure. + properties: + code: + type: string + description: A machine-readable error code. + message: + type: string + description: A human-readable error message. + param: + type: string + description: The parameter that was invalid, usually `training_file` or `validation_file`. This field will be null if the failure was not parameter-specific. + nullable: true + required: + - code + - message + - param + fine_tuned_model: + type: string + nullable: true + description: The name of the fine-tuned model that is being created. The value will be null if the fine-tuning job is still running. + finished_at: + type: integer + nullable: true + description: The Unix timestamp (in seconds) for when the fine-tuning job was finished. The value will be null if the fine-tuning job is still running. + hyperparameters: + type: object + description: The hyperparameters used for the fine-tuning job. See the [fine-tuning guide](/docs/guides/fine-tuning) for more details. + properties: + n_epochs: + oneOf: + - type: string + enum: + - auto + - type: integer + minimum: 1 + maximum: 50 + default: auto + description: |- + The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset. + "auto" decides the optimal number of epochs based on the size of the dataset. If setting the number manually, we support any number between 1 and 50 epochs. + required: + - n_epochs + model: + type: string + description: The base model that is being fine-tuned. + object: + type: string + description: The object type, which is always "fine_tuning.job". + enum: + - fine_tuning.job + organization_id: + type: string + description: The organization that owns the fine-tuning job. + result_files: + type: array + description: The compiled results file ID(s) for the fine-tuning job. You can retrieve the results with the [Files API](/docs/api-reference/files/retrieve-contents). + items: + type: string + example: file-abc123 + status: + type: string + description: The current status of the fine-tuning job, which can be either `validating_files`, `queued`, `running`, `succeeded`, `failed`, or `cancelled`. + enum: + - validating_files + - queued + - running + - succeeded + - failed + - cancelled + trained_tokens: + type: integer + nullable: true + description: The total number of billable tokens processed by this fine-tuning job. The value will be null if the fine-tuning job is still running. + training_file: + type: string + description: The file ID used for training. You can retrieve the training data with the [Files API](/docs/api-reference/files/retrieve-contents). + validation_file: + type: string + nullable: true + description: The file ID used for validation. You can retrieve the validation results with the [Files API](/docs/api-reference/files/retrieve-contents). + integrations: + type: array + nullable: true + description: A list of integrations to enable for this fine-tuning job. + maxItems: 5 + items: + oneOf: + - $ref: '#/components/schemas/FineTuningIntegration' + x-oaiExpandable: true + seed: + type: integer + description: The seed used for the fine-tuning job. + estimated_finish: + type: integer + nullable: true + description: The Unix timestamp (in seconds) for when the fine-tuning job is estimated to finish. The value will be null if the fine-tuning job is not running. + required: + - created_at + - error + - finished_at + - fine_tuned_model + - hyperparameters + - id + - model + - object + - organization_id + - result_files + - status + - trained_tokens + - training_file + - validation_file + - seed + x-oaiMeta: + name: The fine-tuning job object + example: | + { + "object": "fine_tuning.job", + "id": "ftjob-abc123", + "model": "davinci-002", + "created_at": 1692661014, + "finished_at": 1692661190, + "fine_tuned_model": "ft:davinci-002:my-org:custom_suffix:7q8mpxmy", + "organization_id": "org-123", + "result_files": [ + "file-abc123" + ], + "status": "succeeded", + "validation_file": null, + "training_file": "file-abc123", + "hyperparameters": { + "n_epochs": 4, + "batch_size": 1, + "learning_rate_multiplier": 1.0 + }, + "trained_tokens": 5768, + "integrations": [], + "seed": 0, + "estimated_finish": 0 + } + FineTuningIntegration: + type: object + title: Fine-Tuning Job Integration + required: + - type + - wandb + properties: + type: + type: string + description: The type of the integration being enabled for the fine-tuning job + enum: + - wandb + wandb: + type: object + description: | + The settings for your integration with Weights and Biases. This payload specifies the project that + metrics will be sent to. Optionally, you can set an explicit display name for your run, add tags + to your run, and set a default entity (team, username, etc) to be associated with your run. + required: + - project + properties: + project: + description: | + The name of the project that the new run will be created under. + type: string + example: my-wandb-project + name: + description: | + A display name to set for the run. If not set, we will use the Job ID as the name. + nullable: true + type: string + entity: + description: | + The entity to use for the run. This allows you to set the team or username of the WandB user that you would + like associated with the run. If not set, the default entity for the registered WandB API key is used. + nullable: true + type: string + tags: + description: | + A list of tags to be attached to the newly created run. These tags are passed through directly to WandB. Some + default tags are generated by OpenAI: "openai/finetune", "openai/{base-model}", "openai/{ftjob-abcdef}". + type: array + items: + type: string + example: custom-tag + FineTuningJobEvent: + type: object + description: Fine-tuning job event object + properties: + id: + type: string + created_at: + type: integer + level: + type: string + enum: + - info + - warn + - error + message: + type: string + object: + type: string + enum: + - fine_tuning.job.event + required: + - id + - object + - created_at + - level + - message + x-oaiMeta: + name: The fine-tuning job event object + example: | + { + "object": "fine_tuning.job.event", + "id": "ftevent-abc123" + "created_at": 1677610602, + "level": "info", + "message": "Created fine-tuning job" + } + FineTuningJobCheckpoint: + type: object + title: FineTuningJobCheckpoint + description: | + The `fine_tuning.job.checkpoint` object represents a model checkpoint for a fine-tuning job that is ready to use. + properties: + id: + type: string + description: The checkpoint identifier, which can be referenced in the API endpoints. + created_at: + type: integer + description: The Unix timestamp (in seconds) for when the checkpoint was created. + fine_tuned_model_checkpoint: + type: string + description: The name of the fine-tuned checkpoint model that is created. + step_number: + type: integer + description: The step number that the checkpoint was created at. + metrics: + type: object + description: Metrics at the step number during the fine-tuning job. + properties: + step: + type: number + train_loss: + type: number + train_mean_token_accuracy: + type: number + valid_loss: + type: number + valid_mean_token_accuracy: + type: number + full_valid_loss: + type: number + full_valid_mean_token_accuracy: + type: number + fine_tuning_job_id: + type: string + description: The name of the fine-tuning job that this checkpoint was created from. + object: + type: string + description: The object type, which is always "fine_tuning.job.checkpoint". + enum: + - fine_tuning.job.checkpoint + required: + - created_at + - fine_tuning_job_id + - fine_tuned_model_checkpoint + - id + - metrics + - object + - step_number + x-oaiMeta: + name: The fine-tuning job checkpoint object + example: | + { + "object": "fine_tuning.job.checkpoint", + "id": "ftckpt_qtZ5Gyk4BLq1SfLFWp3RtO3P", + "created_at": 1712211699, + "fine_tuned_model_checkpoint": "ft:gpt-4o-mini-2024-07-18:my-org:custom_suffix:9ABel2dg:ckpt-step-88", + "fine_tuning_job_id": "ftjob-fpbNQ3H1GrMehXRf8cO97xTN", + "metrics": { + "step": 88, + "train_loss": 0.478, + "train_mean_token_accuracy": 0.924, + "valid_loss": 10.112, + "valid_mean_token_accuracy": 0.145, + "full_valid_loss": 0.567, + "full_valid_mean_token_accuracy": 0.944 + }, + "step_number": 88 + } + FinetuneChatRequestInput: + type: object + description: The per-line training example of a fine-tuning input file for chat models + properties: + messages: + type: array + minItems: 1 + items: + oneOf: + - $ref: '#/components/schemas/ChatCompletionRequestSystemMessage' + - $ref: '#/components/schemas/ChatCompletionRequestUserMessage' + - $ref: '#/components/schemas/FineTuneChatCompletionRequestAssistantMessage' + - $ref: '#/components/schemas/ChatCompletionRequestToolMessage' + - $ref: '#/components/schemas/ChatCompletionRequestFunctionMessage' + x-oaiExpandable: true + tools: + type: array + description: A list of tools the model may generate JSON inputs for. + items: + $ref: '#/components/schemas/ChatCompletionTool' + parallel_tool_calls: + $ref: '#/components/schemas/ParallelToolCalls' + functions: + deprecated: true + description: A list of functions the model may generate JSON inputs for. + type: array + minItems: 1 + maxItems: 128 + items: + $ref: '#/components/schemas/ChatCompletionFunctions' + x-oaiMeta: + name: Training format for chat models + example: | + { + "messages": [ + { "role": "user", "content": "What is the weather in San Francisco?" }, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_id", + "type": "function", + "function": { + "name": "get_current_weather", + "arguments": "{\"location\": \"San Francisco, USA\", \"format\": \"celsius\"}" + } + } + ] + } + ], + "parallel_tool_calls": false, + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and country, eg. San Francisco, USA" + }, + "format": { "type": "string", "enum": ["celsius", "fahrenheit"] } + }, + "required": ["location", "format"] + } + } + } + ] + } + FinetuneCompletionRequestInput: + type: object + description: The per-line training example of a fine-tuning input file for completions models + properties: + prompt: + type: string + description: The input prompt for this training example. + completion: + type: string + description: The desired completion for this training example. + x-oaiMeta: + name: Training format for completions models + example: | + { + "prompt": "What is the answer to 2+2", + "completion": "4" + } + CompletionUsage: + type: object + description: Usage statistics for the completion request. + properties: + completion_tokens: + type: integer + description: Number of tokens in the generated completion. + prompt_tokens: + type: integer + description: Number of tokens in the prompt. + total_tokens: + type: integer + description: Total number of tokens used in the request (prompt + completion). + required: + - prompt_tokens + - completion_tokens + - total_tokens + RunCompletionUsage: + type: object + description: Usage statistics related to the run. This value will be `null` if the run is not in a terminal state (i.e. `in_progress`, `queued`, etc.). + properties: + completion_tokens: + type: integer + description: Number of completion tokens used over the course of the run. + prompt_tokens: + type: integer + description: Number of prompt tokens used over the course of the run. + total_tokens: + type: integer + description: Total number of tokens used (prompt + completion). + required: + - prompt_tokens + - completion_tokens + - total_tokens + nullable: true + RunStepCompletionUsage: + type: object + description: Usage statistics related to the run step. This value will be `null` while the run step's status is `in_progress`. + properties: + completion_tokens: + type: integer + description: Number of completion tokens used over the course of the run step. + prompt_tokens: + type: integer + description: Number of prompt tokens used over the course of the run step. + total_tokens: + type: integer + description: Total number of tokens used (prompt + completion). + required: + - prompt_tokens + - completion_tokens + - total_tokens + nullable: true + AssistantsApiResponseFormatOption: + description: | + Specifies the format that the model must output. Compatible with [GPT-4o](/docs/models/gpt-4o), [GPT-4 Turbo](/docs/models/gpt-4-turbo-and-gpt-4), and all GPT-3.5 Turbo models since `gpt-3.5-turbo-1106`. + + Setting to `{ "type": "json_schema", "json_schema": {...} }` enables Structured Outputs which guarantees the model will match your supplied JSON schema. Learn more in the [Structured Outputs guide](/docs/guides/structured-outputs). + + Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON. + + **Important:** when using JSON mode, you **must** also instruct the model to produce JSON yourself via a system or user message. Without this, the model may generate an unending stream of whitespace until the generation reaches the token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be partially cut off if `finish_reason="length"`, which indicates the generation exceeded `max_tokens` or the conversation exceeded the max context length. + oneOf: + - type: string + description: | + `auto` is the default value + enum: + - auto + - $ref: '#/components/schemas/ResponseFormatText' + - $ref: '#/components/schemas/ResponseFormatJsonObject' + - $ref: '#/components/schemas/ResponseFormatJsonSchema' + x-oaiExpandable: true + AssistantObject: + type: object + title: Assistant + description: Represents an `assistant` that can call the model and use tools. + properties: + id: + description: The identifier, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `assistant`. + type: string + enum: + - assistant + created_at: + description: The Unix timestamp (in seconds) for when the assistant was created. + type: integer + name: + description: | + The name of the assistant. The maximum length is 256 characters. + type: string + maxLength: 256 + nullable: true + description: + description: | + The description of the assistant. The maximum length is 512 characters. + type: string + maxLength: 512 + nullable: true + model: + description: | + ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models/overview) for descriptions of them. + type: string + instructions: + description: | + The system instructions that the assistant uses. The maximum length is 256,000 characters. + type: string + maxLength: 256000 + nullable: true + tools: + description: | + A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types `code_interpreter`, `file_search`, or `function`. + default: [] + type: array + maxItems: 128 + items: + oneOf: + - $ref: '#/components/schemas/AssistantToolsCode' + - $ref: '#/components/schemas/AssistantToolsFileSearch' + - $ref: '#/components/schemas/AssistantToolsFunction' + x-oaiExpandable: true + tool_resources: + type: object + description: | + A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs. + properties: + code_interpreter: + type: object + properties: + file_ids: + type: array + description: | + A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter`` tool. There can be a maximum of 20 files associated with the tool. + default: [] + maxItems: 20 + items: + type: string + file_search: + type: object + properties: + vector_store_ids: + type: array + description: | + The ID of the [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant. + maxItems: 1 + items: + type: string + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + temperature: + description: | + What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + type: number + minimum: 0 + maximum: 2 + default: 1 + example: 1 + nullable: true + top_p: + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + nullable: true + description: | + An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or temperature but not both. + response_format: + $ref: '#/components/schemas/AssistantsApiResponseFormatOption' + nullable: true + required: + - id + - object + - created_at + - name + - description + - model + - instructions + - tools + - metadata + x-oaiMeta: + name: The assistant object + beta: true + example: | + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": "Math Tutor", + "description": null, + "model": "gpt-4o", + "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + "tools": [ + { + "type": "code_interpreter" + } + ], + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + } + CreateAssistantRequest: + type: object + additionalProperties: false + properties: + model: + description: | + ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models/overview) for descriptions of them. + example: gpt-4o + anyOf: + - type: string + - type: string + enum: + - gpt-4o + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o-2024-08-06 + - gpt-4o-mini + - gpt-4o-mini-2024-07-18 + - gpt-4-turbo + - gpt-4-turbo-2024-04-09 + - gpt-4-0125-preview + - gpt-4-turbo-preview + - gpt-4-1106-preview + - gpt-4-vision-preview + - gpt-4 + - gpt-4-0314 + - gpt-4-0613 + - gpt-4-32k + - gpt-4-32k-0314 + - gpt-4-32k-0613 + - gpt-3.5-turbo + - gpt-3.5-turbo-16k + - gpt-3.5-turbo-0613 + - gpt-3.5-turbo-1106 + - gpt-3.5-turbo-0125 + - gpt-3.5-turbo-16k-0613 + x-oaiTypeLabel: string + name: + description: | + The name of the assistant. The maximum length is 256 characters. + type: string + nullable: true + maxLength: 256 + description: + description: | + The description of the assistant. The maximum length is 512 characters. + type: string + nullable: true + maxLength: 512 + instructions: + description: | + The system instructions that the assistant uses. The maximum length is 256,000 characters. + type: string + nullable: true + maxLength: 256000 + tools: + description: | + A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types `code_interpreter`, `file_search`, or `function`. + default: [] + type: array + maxItems: 128 + items: + oneOf: + - $ref: '#/components/schemas/AssistantToolsCode' + - $ref: '#/components/schemas/AssistantToolsFileSearch' + - $ref: '#/components/schemas/AssistantToolsFunction' + x-oaiExpandable: true + tool_resources: + type: object + description: | + A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs. + properties: + code_interpreter: + type: object + properties: + file_ids: + type: array + description: | + A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + default: [] + maxItems: 20 + items: + type: string + file_search: + type: object + properties: + vector_store_ids: + type: array + description: | + The [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant. + maxItems: 1 + items: + type: string + vector_stores: + type: array + description: | + A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this assistant. There can be a maximum of 1 vector store attached to the assistant. + maxItems: 1 + items: + type: object + properties: + file_ids: + type: array + description: | + A list of [file](/docs/api-reference/files) IDs to add to the vector store. There can be a maximum of 10000 files in a vector store. + maxItems: 10000 + items: + type: string + chunking_strategy: + type: object + description: The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy. + oneOf: + - type: object + title: Auto Chunking Strategy + description: The default strategy. This strategy currently uses a `max_chunk_size_tokens` of `800` and `chunk_overlap_tokens` of `400`. + additionalProperties: false + properties: + type: + type: string + description: Always `auto`. + enum: + - auto + required: + - type + - type: object + title: Static Chunking Strategy + additionalProperties: false + properties: + type: + type: string + description: Always `static`. + enum: + - static + static: + type: object + additionalProperties: false + properties: + max_chunk_size_tokens: + type: integer + minimum: 100 + maximum: 4096 + description: The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`. + chunk_overlap_tokens: + type: integer + description: | + The number of tokens that overlap between chunks. The default value is `400`. + + Note that the overlap must not exceed half of `max_chunk_size_tokens`. + required: + - max_chunk_size_tokens + - chunk_overlap_tokens + required: + - type + - static + x-oaiExpandable: true + metadata: + type: object + description: | + Set of 16 key-value pairs that can be attached to a vector store. This can be useful for storing additional information about the vector store in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + x-oaiTypeLabel: map + oneOf: + - required: + - vector_store_ids + - required: + - vector_stores + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + temperature: + description: | + What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + type: number + minimum: 0 + maximum: 2 + default: 1 + example: 1 + nullable: true + top_p: + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + nullable: true + description: | + An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or temperature but not both. + response_format: + $ref: '#/components/schemas/AssistantsApiResponseFormatOption' + nullable: true + required: + - model + Response: + allOf: + - $ref: "#/components/schemas/ModelResponseProperties" + - $ref: "#/components/schemas/ResponseProperties" + - type: object + properties: + id: + type: string + description: | + Unique identifier for this Response. + object: + type: string + description: | + The object type of this resource - always set to `response`. + enum: + - response + x-stainless-const: true + status: + type: string + description: > + The status of the response generation. One of `completed`, + `failed`, + + `in_progress`, or `incomplete`. + enum: + - completed + - failed + - in_progress + - incomplete + created_at: + type: number + description: | + Unix timestamp (in seconds) of when this Response was created. + error: + $ref: "#/components/schemas/ResponseError" + incomplete_details: + type: object + nullable: true + description: | + Details about why the response is incomplete. + properties: + reason: + type: string + description: The reason why the response is incomplete. + enum: + - max_output_tokens + - content_filter + output: + type: array + description: > + An array of content items generated by the model. + + + - The length and order of items in the `output` array is + dependent + on the model's response. + - Rather than accessing the first item in the `output` array + and + assuming it's an `assistant` message with the content generated by + the model, you might consider using the `output_text` property where + supported in SDKs. + items: + $ref: "#/components/schemas/OutputItem" + output_text: + type: string + nullable: true + description: > + SDK-only convenience property that contains the aggregated text + output + + from all `output_text` items in the `output` array, if any are + present. + + Supported in the Python and JavaScript SDKs. + x-oaiSupportedSDKs: + - python + - javascript + usage: + $ref: "#/components/schemas/ResponseUsage" + parallel_tool_calls: + type: boolean + description: | + Whether to allow the model to run tool calls in parallel. + default: true + required: + - id + - object + - created_at + - error + - incomplete_details + - instructions + - model + - tools + - output + - parallel_tool_calls + - metadata + - tool_choice + - temperature + - top_p + x-oaiMeta: + name: The response object + group: responses + example: > + { + "id": "resp_67ccd3a9da748190baa7f1570fe91ac604becb25c45c1d41", + "object": "response", + "created_at": 1741476777, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "model": "gpt-4o-2024-08-06", + "output": [ + { + "type": "message", + "id": "msg_67ccd3acc8d48190a77525dc6de64b4104becb25c45c1d41", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "The image depicts a scenic landscape with a wooden boardwalk or pathway leading through lush, green grass under a blue sky with some clouds. The setting suggests a peaceful natural area, possibly a park or nature reserve. There are trees and shrubs in the background.", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": { + "effort": null, + "summary": null + }, + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + } + }, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 328, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 52, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 380 + }, + "user": null, + "metadata": {} + } + ModelResponseProperties: + type: object + properties: + metadata: + $ref: "#/components/schemas/Metadata" + temperature: + type: number + minimum: 0 + maximum: 2 + default: 1 + example: 1 + nullable: true + description: > + What sampling temperature to use, between 0 and 2. Higher values + like 0.8 will make the output more random, while lower values like + 0.2 will make it more focused and deterministic. + + We generally recommend altering this or `top_p` but not both. + top_p: + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + nullable: true + description: > + An alternative to sampling with temperature, called nucleus + sampling, + + where the model considers the results of the tokens with top_p + probability + + mass. So 0.1 means only the tokens comprising the top 10% + probability mass + + are considered. + + + We generally recommend altering this or `temperature` but not both. + user: + type: string + example: user-1234 + description: > + A unique identifier representing your end-user, which can help + OpenAI to monitor and detect abuse. [Learn + more](/docs/guides/safety-best-practices#end-user-ids). + service_tier: + $ref: "#/components/schemas/ServiceTier" + Metadata: + type: object + description: > + Set of 16 key-value pairs that can be attached to an object. This can be + + useful for storing additional information about the object in a + structured + + format, and querying for objects via API or the dashboard. + + + Keys are strings with a maximum length of 64 characters. Values are + strings + + with a maximum length of 512 characters. + additionalProperties: + type: string + x-oaiTypeLabel: map + nullable: true + ServiceTier: + type: string + description: > + Specifies the latency tier to use for processing the request. This + parameter is relevant for customers subscribed to the scale tier + service: + - If set to 'auto', and the Project is Scale tier enabled, the system + will utilize scale tier credits until they are exhausted. + - If set to 'auto', and the Project is not Scale tier enabled, the request will be processed using the default service tier with a lower uptime SLA and no latency guarentee. + - If set to 'default', the request will be processed using the default service tier with a lower uptime SLA and no latency guarentee. + - If set to 'flex', the request will be processed with the Flex Processing service tier. [Learn more](/docs/guides/flex-processing). + - When not set, the default behavior is 'auto'. + + When this parameter is set, the response body will include the `service_tier` utilized. + enum: + - auto + - default + - flex + nullable: true + default: auto + ResponseUsage: + type: object + description: | + Represents token usage details including input tokens, output tokens, + a breakdown of output tokens, and the total tokens used. + properties: + input_tokens: + type: integer + description: The number of input tokens. + input_tokens_details: + type: object + description: A detailed breakdown of the input tokens. + properties: + cached_tokens: + type: integer + description: | + The number of tokens that were retrieved from the cache. + [More on prompt caching](/docs/guides/prompt-caching). + required: + - cached_tokens + output_tokens: + type: integer + description: The number of output tokens. + output_tokens_details: + type: object + description: A detailed breakdown of the output tokens. + properties: + reasoning_tokens: + type: integer + description: The number of reasoning tokens. + required: + - reasoning_tokens + total_tokens: + type: integer + description: The total number of tokens used. + required: + - input_tokens + - input_tokens_details + - output_tokens + - output_tokens_details + - total_tokens + ResponseError: + type: object + description: | + An error object returned when the model fails to generate a Response. + nullable: true + properties: + code: + $ref: "#/components/schemas/ResponseErrorCode" + message: + type: string + description: | + A human-readable description of the error. + required: + - code + - message + ResponseErrorCode: + type: string + description: | + The error code for the response. + enum: + - server_error + - rate_limit_exceeded + - invalid_prompt + - vector_store_timeout + - invalid_image + - invalid_image_format + - invalid_base64_image + - invalid_image_url + - image_too_large + - image_too_small + - image_parse_error + - image_content_policy_violation + - invalid_image_mode + - image_file_too_large + - unsupported_image_media_type + - empty_image_file + - failed_to_download_image + - image_file_not_found + OutputItem: + anyOf: + - $ref: "#/components/schemas/OutputMessage" + - $ref: "#/components/schemas/FileSearchToolCall" + - $ref: "#/components/schemas/FunctionToolCall" + - $ref: "#/components/schemas/WebSearchToolCall" + - $ref: "#/components/schemas/ComputerToolCall" + - $ref: "#/components/schemas/ReasoningItem" + discriminator: + propertyName: type + ReasoningItem: + type: object + description: > + A description of the chain of thought used by a reasoning model while + generating + + a response. + title: Reasoning + properties: + type: + type: string + description: | + The type of the object. Always `reasoning`. + enum: + - reasoning + x-stainless-const: true + id: + type: string + description: | + The unique identifier of the reasoning content. + summary: + type: array + description: | + Reasoning text contents. + items: + type: object + properties: + type: + type: string + description: | + The type of the object. Always `summary_text`. + enum: + - summary_text + x-stainless-const: true + text: + type: string + description: > + A short summary of the reasoning used by the model when + generating + + the response. + required: + - type + - text + status: + type: string + description: | + The status of the item. One of `in_progress`, `completed`, or + `incomplete`. Populated when items are returned via API. + enum: + - in_progress + - completed + - incomplete + required: + - id + - summary + - type + FileSearchToolCall: + type: object + title: File search tool call + description: > + The results of a file search tool call. See the + + [file search guide](/docs/guides/tools-file-search) for more + information. + properties: + id: + type: string + description: | + The unique ID of the file search tool call. + type: + type: string + enum: + - file_search_call + description: | + The type of the file search tool call. Always `file_search_call`. + x-stainless-const: true + status: + type: string + description: | + The status of the file search tool call. One of `in_progress`, + `searching`, `incomplete` or `failed`, + enum: + - in_progress + - searching + - completed + - incomplete + - failed + queries: + type: array + items: + type: string + description: | + The queries used to search for files. + results: + type: array + description: | + The results of the file search tool call. + items: + type: object + properties: + file_id: + type: string + description: | + The unique ID of the file. + text: + type: string + description: | + The text that was retrieved from the file. + filename: + type: string + description: | + The name of the file. + attributes: + $ref: "#/components/schemas/VectorStoreFileAttributes" + score: + type: number + format: float + description: | + The relevance score of the file - a value between 0 and 1. + nullable: true + required: + - id + - type + - status + - queries + FunctionToolCall: + type: object + title: Function tool call + description: > + A tool call to run a function. See the + + [function calling guide](/docs/guides/function-calling) for more + information. + properties: + id: + type: string + description: | + The unique ID of the function tool call. + type: + type: string + enum: + - function_call + description: | + The type of the function tool call. Always `function_call`. + x-stainless-const: true + call_id: + type: string + description: | + The unique ID of the function tool call generated by the model. + name: + type: string + description: | + The name of the function to run. + arguments: + type: string + description: | + A JSON string of the arguments to pass to the function. + status: + type: string + description: | + The status of the item. One of `in_progress`, `completed`, or + `incomplete`. Populated when items are returned via API. + enum: + - in_progress + - completed + - incomplete + required: + - type + - call_id + - name + - arguments + VectorStoreFileAttributes: + type: object + description: > + Set of 16 key-value pairs that can be attached to an object. This can + be + + useful for storing additional information about the object in a + structured + + format, and querying for objects via API or the dashboard. Keys are + strings + + with a maximum length of 64 characters. Values are strings with a + maximum + + length of 512 characters, booleans, or numbers. + maxProperties: 16 + additionalProperties: + oneOf: + - type: string + maxLength: 512 + - type: number + - type: boolean + x-oaiTypeLabel: map + nullable: true + OutputMessage: + type: object + title: Output message + description: | + An output message from the model. + properties: + id: + type: string + description: | + The unique ID of the output message. + type: + type: string + description: | + The type of the output message. Always `message`. + enum: + - message + x-stainless-const: true + role: + type: string + description: | + The role of the output message. Always `assistant`. + enum: + - assistant + x-stainless-const: true + content: + type: array + description: | + The content of the output message. + items: + $ref: "#/components/schemas/OutputContent" + status: + type: string + description: > + The status of the message input. One of `in_progress`, `completed`, + or + + `incomplete`. Populated when input items are returned via API. + enum: + - in_progress + - completed + - incomplete + required: + - id + - type + - role + - content + - status + WebSearchToolCall: + type: object + title: Web search tool call + description: | + The results of a web search tool call. See the + [web search guide](/docs/guides/tools-web-search) for more information. + properties: + id: + type: string + description: | + The unique ID of the web search tool call. + type: + type: string + enum: + - web_search_call + description: | + The type of the web search tool call. Always `web_search_call`. + x-stainless-const: true + status: + type: string + description: | + The status of the web search tool call. + enum: + - in_progress + - searching + - completed + - failed + required: + - id + - type + - status + OutputContent: + oneOf: + - $ref: "#/components/schemas/OutputTextContent" + - $ref: "#/components/schemas/RefusalContent" + OutputTextContent: + properties: + type: + type: string + enum: + - output_text + description: The type of the output text. Always `output_text`. + default: output_text + x-stainless-const: true + text: + type: string + description: The text output from the model. + annotations: + items: + $ref: "#/components/schemas/Annotation" + type: array + description: The annotations of the text output. + type: object + required: + - type + - text + - annotations + title: Output text + description: A text output from the model. + RefusalContent: + properties: + type: + type: string + enum: + - refusal + description: The type of the refusal. Always `refusal`. + default: refusal + x-stainless-const: true + refusal: + type: string + description: The refusal explanationfrom the model. + type: object + required: + - type + - refusal + title: Refusal + description: A refusal from the model. + Annotation: + oneOf: + - $ref: "#/components/schemas/FileCitationBody" + - $ref: "#/components/schemas/UrlCitationBody" + - $ref: "#/components/schemas/FilePath" + discriminator: + propertyName: type + FilePath: + type: object + title: File path + description: | + A path to a file. + properties: + type: + type: string + description: | + The type of the file path. Always `file_path`. + enum: + - file_path + x-stainless-const: true + file_id: + type: string + description: | + The ID of the file. + index: + type: integer + description: | + The index of the file in the list of files. + required: + - type + - file_id + - index + UrlCitationBody: + properties: + type: + type: string + enum: + - url_citation + description: The type of the URL citation. Always `url_citation`. + default: url_citation + x-stainless-const: true + url: + type: string + description: The URL of the web resource. + start_index: + type: integer + description: The index of the first character of the URL citation in the message. + end_index: + type: integer + description: The index of the last character of the URL citation in the message. + title: + type: string + description: The title of the web resource. + type: object + required: + - type + - url + - start_index + - end_index + - title + title: URL citation + description: A citation for a web resource used to generate a model response. + FileCitationBody: + properties: + type: + type: string + enum: + - file_citation + description: The type of the file citation. Always `file_citation`. + default: file_citation + x-stainless-const: true + file_id: + type: string + description: The ID of the file. + index: + type: integer + description: The index of the file in the list of files. + type: object + required: + - type + - file_id + - index + title: File citation + description: A citation to a file. + ComputerToolCall: + type: object + title: Computer tool call + description: > + A tool call to a computer use tool. See the + + [computer use guide](/docs/guides/tools-computer-use) for more + information. + properties: + type: + type: string + description: The type of the computer call. Always `computer_call`. + enum: + - computer_call + default: computer_call + id: + type: string + description: The unique ID of the computer call. + call_id: + type: string + description: | + An identifier used when responding to the tool call with output. + action: + $ref: "#/components/schemas/ComputerAction" + pending_safety_checks: + type: array + items: + $ref: "#/components/schemas/ComputerToolCallSafetyCheck" + description: | + The pending safety checks for the computer call. + status: + type: string + description: | + The status of the item. One of `in_progress`, `completed`, or + `incomplete`. Populated when items are returned via API. + enum: + - in_progress + - completed + - incomplete + required: + - type + - id + - action + - call_id + - pending_safety_checks + - status + ModifyAssistantRequest: + type: object + additionalProperties: false + properties: + model: + description: | + ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models/overview) for descriptions of them. + anyOf: + - type: string + name: + description: | + The name of the assistant. The maximum length is 256 characters. + type: string + nullable: true + maxLength: 256 + description: + description: | + The description of the assistant. The maximum length is 512 characters. + type: string + nullable: true + maxLength: 512 + instructions: + description: | + The system instructions that the assistant uses. The maximum length is 256,000 characters. + type: string + nullable: true + maxLength: 256000 + tools: + description: | + A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types `code_interpreter`, `file_search`, or `function`. + default: [] + type: array + maxItems: 128 + items: + oneOf: + - $ref: '#/components/schemas/AssistantToolsCode' + - $ref: '#/components/schemas/AssistantToolsFileSearch' + - $ref: '#/components/schemas/AssistantToolsFunction' + x-oaiExpandable: true + tool_resources: + type: object + description: | + A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs. + properties: + code_interpreter: + type: object + properties: + file_ids: + type: array + description: | + Overrides the list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + default: [] + maxItems: 20 + items: + type: string + file_search: + type: object + properties: + vector_store_ids: + type: array + description: | + Overrides the [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant. + maxItems: 1 + items: + type: string + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + temperature: + description: | + What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + type: number + minimum: 0 + maximum: 2 + default: 1 + example: 1 + nullable: true + top_p: + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + nullable: true + description: | + An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or temperature but not both. + response_format: + $ref: '#/components/schemas/AssistantsApiResponseFormatOption' + nullable: true + ComputerAction: + oneOf: + - $ref: "#/components/schemas/Click" + - $ref: "#/components/schemas/DoubleClick" + - $ref: "#/components/schemas/Drag" + - $ref: "#/components/schemas/KeyPress" + - $ref: "#/components/schemas/Move" + - $ref: "#/components/schemas/Screenshot" + - $ref: "#/components/schemas/Scroll" + - $ref: "#/components/schemas/Type" + - $ref: "#/components/schemas/Wait" + DoubleClick: + type: object + title: DoubleClick + description: | + A double click action. + properties: + type: + type: string + enum: + - double_click + default: double_click + description: > + Specifies the event type. For a double click action, this property + is + + always set to `double_click`. + x-stainless-const: true + x: + type: integer + description: | + The x-coordinate where the double click occurred. + y: + type: integer + description: | + The y-coordinate where the double click occurred. + required: + - type + - x + - y + Drag: + type: object + title: Drag + description: | + A drag action. + properties: + type: + type: string + enum: + - drag + default: drag + description: | + Specifies the event type. For a drag action, this property is + always set to `drag`. + x-stainless-const: true + path: + type: array + description: > + An array of coordinates representing the path of the drag action. + Coordinates will appear as an array + + of objects, eg + + ``` + + [ + { x: 100, y: 200 }, + { x: 200, y: 300 } + ] + + ``` + items: + title: Drag path coordinates + description: | + A series of x/y coordinate pairs in the drag path. + $ref: "#/components/schemas/Coordinate" + required: + - type + - path + Click: + type: object + title: Click + description: | + A click action. + properties: + type: + type: string + enum: + - click + default: click + description: | + Specifies the event type. For a click action, this property is + always set to `click`. + x-stainless-const: true + button: + type: string + enum: + - left + - right + - wheel + - back + - forward + description: > + Indicates which mouse button was pressed during the click. One of + `left`, `right`, `wheel`, `back`, or `forward`. + x: + type: integer + description: | + The x-coordinate where the click occurred. + y: + type: integer + description: | + The y-coordinate where the click occurred. + required: + - type + - button + - x + - y + Coordinate: + type: object + title: Coordinate + description: | + An x/y coordinate pair, e.g. `{ x: 100, y: 200 }`. + properties: + x: + type: integer + description: | + The x-coordinate. + y: + type: integer + description: | + The y-coordinate. + required: + - x + - y + KeyPress: + type: object + title: KeyPress + description: | + A collection of keypresses the model would like to perform. + properties: + type: + type: string + enum: + - keypress + default: keypress + description: | + Specifies the event type. For a keypress action, this property is + always set to `keypress`. + x-stainless-const: true + keys: + type: array + items: + type: string + description: | + One of the keys the model is requesting to be pressed. + description: > + The combination of keys the model is requesting to be pressed. This + is an + + array of strings, each representing a key. + required: + - type + - keys + Move: + type: object + title: Move + description: | + A mouse move action. + properties: + type: + type: string + enum: + - move + default: move + description: | + Specifies the event type. For a move action, this property is + always set to `move`. + x-stainless-const: true + x: + type: integer + description: | + The x-coordinate to move to. + y: + type: integer + description: | + The y-coordinate to move to. + required: + - type + - x + - y + Screenshot: + type: object + title: Screenshot + description: | + A screenshot action. + properties: + type: + type: string + enum: + - screenshot + default: screenshot + description: | + Specifies the event type. For a screenshot action, this property is + always set to `screenshot`. + x-stainless-const: true + required: + - type + Type: + type: object + title: Type + description: | + An action to type in text. + properties: + type: + type: string + enum: + - type + default: type + description: | + Specifies the event type. For a type action, this property is + always set to `type`. + x-stainless-const: true + text: + type: string + description: | + The text to type. + required: + - type + - text + ComputerToolCallSafetyCheck: + type: object + description: | + A pending safety check for the computer call. + properties: + id: + type: string + description: The ID of the pending safety check. + code: + type: string + description: The type of the pending safety check. + message: + type: string + description: Details about the pending safety check. + required: + - id + - code + - message + DeleteAssistantResponse: + type: object + properties: + id: + type: string + deleted: + type: boolean + object: + type: string + enum: + - assistant.deleted + required: + - id + - object + - deleted + Scroll: + type: object + title: Scroll + description: | + A scroll action. + properties: + type: + type: string + enum: + - scroll + default: scroll + description: | + Specifies the event type. For a scroll action, this property is + always set to `scroll`. + x-stainless-const: true + x: + type: integer + description: | + The x-coordinate where the scroll occurred. + y: + type: integer + description: | + The y-coordinate where the scroll occurred. + scroll_x: + type: integer + description: | + The horizontal scroll distance. + scroll_y: + type: integer + description: | + The vertical scroll distance. + required: + - type + - x + - y + - scroll_x + - scroll_y + Wait: + type: object + title: Wait + description: | + A wait action. + properties: + type: + type: string + enum: + - wait + default: wait + description: | + Specifies the event type. For a wait action, this property is + always set to `wait`. + x-stainless-const: true + required: + - type + ListAssistantsResponse: + type: object + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: '#/components/schemas/AssistantObject' + first_id: + type: string + example: asst_abc123 + last_id: + type: string + example: asst_abc456 + has_more: + type: boolean + example: false + required: + - object + - data + - first_id + - last_id + - has_more + x-oaiMeta: + name: List assistants response object + group: chat + example: | + { + "object": "list", + "data": [ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698982736, + "name": "Coding Tutor", + "description": null, + "model": "gpt-4o", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "tool_resources": {}, + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + }, + { + "id": "asst_abc456", + "object": "assistant", + "created_at": 1698982718, + "name": "My Assistant", + "description": null, + "model": "gpt-4o", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "tool_resources": {}, + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + }, + { + "id": "asst_abc789", + "object": "assistant", + "created_at": 1698982643, + "name": null, + "description": null, + "model": "gpt-4o", + "instructions": null, + "tools": [], + "tool_resources": {}, + "metadata": {}, + "top_p": 1.0, + "temperature": 1.0, + "response_format": "auto" + } + ], + "first_id": "asst_abc123", + "last_id": "asst_abc789", + "has_more": false + } + AssistantToolsCode: + type: object + title: Code interpreter tool + properties: + type: + type: string + description: 'The type of tool being defined: `code_interpreter`' + enum: + - code_interpreter + required: + - type + AssistantToolsFileSearch: + type: object + title: FileSearch tool + properties: + type: + type: string + description: 'The type of tool being defined: `file_search`' + enum: + - file_search + file_search: + type: object + description: Overrides for the file search tool. + properties: + max_num_results: + type: integer + minimum: 1 + maximum: 50 + description: | + The maximum number of results the file search tool should output. The default is 20 for `gpt-4*` models and 5 for `gpt-3.5-turbo`. This number should be between 1 and 50 inclusive. + + Note that the file search tool may output fewer than `max_num_results` results. See the [file search tool documentation](/docs/assistants/tools/file-search/number-of-chunks-returned) for more information. + required: + - type + AssistantToolsFileSearchTypeOnly: + type: object + title: FileSearch tool + properties: + type: + type: string + description: 'The type of tool being defined: `file_search`' + enum: + - file_search + required: + - type + AssistantToolsFunction: + type: object + title: Function tool + properties: + type: + type: string + description: 'The type of tool being defined: `function`' + enum: + - function + function: + $ref: '#/components/schemas/FunctionObject' + required: + - type + - function + TruncationObject: + type: object + title: Thread Truncation Controls + description: Controls for how a thread will be truncated prior to the run. Use this to control the intial context window of the run. + properties: + type: + type: string + description: The truncation strategy to use for the thread. The default is `auto`. If set to `last_messages`, the thread will be truncated to the n most recent messages in the thread. When set to `auto`, messages in the middle of the thread will be dropped to fit the context length of the model, `max_prompt_tokens`. + enum: + - auto + - last_messages + last_messages: + type: integer + description: The number of most recent messages from the thread when constructing the context for the run. + minimum: 1 + nullable: true + required: + - type + AssistantsApiToolChoiceOption: + description: | + Controls which (if any) tool is called by the model. + `none` means the model will not call any tools and instead generates a message. + `auto` is the default value and means the model can pick between generating a message or calling one or more tools. + `required` means the model must call one or more tools before responding to the user. + Specifying a particular tool like `{"type": "file_search"}` or `{"type": "function", "function": {"name": "my_function"}}` forces the model to call that tool. + oneOf: + - type: string + description: | + `none` means the model will not call any tools and instead generates a message. `auto` means the model can pick between generating a message or calling one or more tools. `required` means the model must call one or more tools before responding to the user. + enum: + - none + - auto + - required + - $ref: '#/components/schemas/AssistantsNamedToolChoice' + x-oaiExpandable: true + AssistantsNamedToolChoice: + type: object + description: Specifies a tool the model should use. Use to force the model to call a specific tool. + properties: + type: + type: string + enum: + - function + - code_interpreter + - file_search + description: The type of the tool. If type is `function`, the function name must be set + function: + type: object + properties: + name: + type: string + description: The name of the function to call. + required: + - name + required: + - type + RunObject: + type: object + title: A run on a thread + description: Represents an execution run on a [thread](/docs/api-reference/threads). + properties: + id: + description: The identifier, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `thread.run`. + type: string + enum: + - thread.run + created_at: + description: The Unix timestamp (in seconds) for when the run was created. + type: integer + thread_id: + description: The ID of the [thread](/docs/api-reference/threads) that was executed on as a part of this run. + type: string + assistant_id: + description: The ID of the [assistant](/docs/api-reference/assistants) used for execution of this run. + type: string + status: + description: The status of the run, which can be either `queued`, `in_progress`, `requires_action`, `cancelling`, `cancelled`, `failed`, `completed`, `incomplete`, or `expired`. + type: string + enum: + - queued + - in_progress + - requires_action + - cancelling + - cancelled + - failed + - completed + - incomplete + - expired + required_action: + type: object + description: Details on the action required to continue the run. Will be `null` if no action is required. + nullable: true + properties: + type: + description: For now, this is always `submit_tool_outputs`. + type: string + enum: + - submit_tool_outputs + submit_tool_outputs: + type: object + description: Details on the tool outputs needed for this run to continue. + properties: + tool_calls: + type: array + description: A list of the relevant tool calls. + items: + $ref: '#/components/schemas/RunToolCallObject' + required: + - tool_calls + required: + - type + - submit_tool_outputs + last_error: + type: object + description: The last error associated with this run. Will be `null` if there are no errors. + nullable: true + properties: + code: + type: string + description: One of `server_error`, `rate_limit_exceeded`, or `invalid_prompt`. + enum: + - server_error + - rate_limit_exceeded + - invalid_prompt + message: + type: string + description: A human-readable description of the error. + required: + - code + - message + expires_at: + description: The Unix timestamp (in seconds) for when the run will expire. + type: integer + nullable: true + started_at: + description: The Unix timestamp (in seconds) for when the run was started. + type: integer + nullable: true + cancelled_at: + description: The Unix timestamp (in seconds) for when the run was cancelled. + type: integer + nullable: true + failed_at: + description: The Unix timestamp (in seconds) for when the run failed. + type: integer + nullable: true + completed_at: + description: The Unix timestamp (in seconds) for when the run was completed. + type: integer + nullable: true + incomplete_details: + description: Details on why the run is incomplete. Will be `null` if the run is not incomplete. + type: object + nullable: true + properties: + reason: + description: The reason why the run is incomplete. This will point to which specific token limit was reached over the course of the run. + type: string + enum: + - max_completion_tokens + - max_prompt_tokens + model: + description: The model that the [assistant](/docs/api-reference/assistants) used for this run. + type: string + instructions: + description: The instructions that the [assistant](/docs/api-reference/assistants) used for this run. + type: string + tools: + description: The list of tools that the [assistant](/docs/api-reference/assistants) used for this run. + default: [] + type: array + maxItems: 20 + items: + oneOf: + - $ref: '#/components/schemas/AssistantToolsCode' + - $ref: '#/components/schemas/AssistantToolsFileSearch' + - $ref: '#/components/schemas/AssistantToolsFunction' + x-oaiExpandable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + usage: + $ref: '#/components/schemas/RunCompletionUsage' + temperature: + description: The sampling temperature used for this run. If not set, defaults to 1. + type: number + nullable: true + top_p: + description: The nucleus sampling value used for this run. If not set, defaults to 1. + type: number + nullable: true + max_prompt_tokens: + type: integer + nullable: true + description: | + The maximum number of prompt tokens specified to have been used over the course of the run. + minimum: 256 + max_completion_tokens: + type: integer + nullable: true + description: | + The maximum number of completion tokens specified to have been used over the course of the run. + minimum: 256 + truncation_strategy: + $ref: '#/components/schemas/TruncationObject' + nullable: true + tool_choice: + $ref: '#/components/schemas/AssistantsApiToolChoiceOption' + nullable: true + parallel_tool_calls: + $ref: '#/components/schemas/ParallelToolCalls' + response_format: + $ref: '#/components/schemas/AssistantsApiResponseFormatOption' + nullable: true + required: + - id + - object + - created_at + - thread_id + - assistant_id + - status + - required_action + - last_error + - expires_at + - started_at + - cancelled_at + - failed_at + - completed_at + - model + - instructions + - tools + - metadata + - usage + - incomplete_details + - max_prompt_tokens + - max_completion_tokens + - truncation_strategy + - tool_choice + - parallel_tool_calls + - response_format + x-oaiMeta: + name: The run object + beta: true + example: | + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1698107661, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "completed", + "started_at": 1699073476, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699073498, + "last_error": null, + "model": "gpt-4o", + "instructions": null, + "tools": [{"type": "file_search"}, {"type": "code_interpreter"}], + "metadata": {}, + "incomplete_details": null, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + }, + "temperature": 1.0, + "top_p": 1.0, + "max_prompt_tokens": 1000, + "max_completion_tokens": 1000, + "truncation_strategy": { + "type": "auto", + "last_messages": null + }, + "response_format": "auto", + "tool_choice": "auto", + "parallel_tool_calls": true + } + CreateRunRequest: + type: object + additionalProperties: false + properties: + assistant_id: + description: The ID of the [assistant](/docs/api-reference/assistants) to use to execute this run. + type: string + model: + description: The ID of the [Model](/docs/api-reference/models) to be used to execute this run. If a value is provided here, it will override the model associated with the assistant. If not, the model associated with the assistant will be used. + example: gpt-4o + anyOf: + - type: string + - type: string + enum: + - gpt-4o + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o-2024-08-06 + - gpt-4o-mini + - gpt-4o-mini-2024-07-18 + - gpt-4-turbo + - gpt-4-turbo-2024-04-09 + - gpt-4-0125-preview + - gpt-4-turbo-preview + - gpt-4-1106-preview + - gpt-4-vision-preview + - gpt-4 + - gpt-4-0314 + - gpt-4-0613 + - gpt-4-32k + - gpt-4-32k-0314 + - gpt-4-32k-0613 + - gpt-3.5-turbo + - gpt-3.5-turbo-16k + - gpt-3.5-turbo-0613 + - gpt-3.5-turbo-1106 + - gpt-3.5-turbo-0125 + - gpt-3.5-turbo-16k-0613 + x-oaiTypeLabel: string + nullable: true + instructions: + description: Overrides the [instructions](/docs/api-reference/assistants/createAssistant) of the assistant. This is useful for modifying the behavior on a per-run basis. + type: string + nullable: true + additional_instructions: + description: Appends additional instructions at the end of the instructions for the run. This is useful for modifying the behavior on a per-run basis without overriding other instructions. + type: string + nullable: true + additional_messages: + description: Adds additional messages to the thread before creating the run. + type: array + items: + $ref: '#/components/schemas/CreateMessageRequest' + nullable: true + tools: + description: Override the tools the assistant can use for this run. This is useful for modifying the behavior on a per-run basis. + nullable: true + type: array + maxItems: 20 + items: + oneOf: + - $ref: '#/components/schemas/AssistantToolsCode' + - $ref: '#/components/schemas/AssistantToolsFileSearch' + - $ref: '#/components/schemas/AssistantToolsFunction' + x-oaiExpandable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + temperature: + type: number + minimum: 0 + maximum: 2 + default: 1 + example: 1 + nullable: true + description: | + What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + top_p: + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + nullable: true + description: | + An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or temperature but not both. + stream: + type: boolean + nullable: true + description: | + If `true`, returns a stream of events that happen during the Run as server-sent events, terminating when the Run enters a terminal state with a `data: [DONE]` message. + max_prompt_tokens: + type: integer + nullable: true + description: | + The maximum number of prompt tokens that may be used over the course of the run. The run will make a best effort to use only the number of prompt tokens specified, across multiple turns of the run. If the run exceeds the number of prompt tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info. + minimum: 256 + max_completion_tokens: + type: integer + nullable: true + description: | + The maximum number of completion tokens that may be used over the course of the run. The run will make a best effort to use only the number of completion tokens specified, across multiple turns of the run. If the run exceeds the number of completion tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info. + minimum: 256 + truncation_strategy: + $ref: '#/components/schemas/TruncationObject' + nullable: true + tool_choice: + $ref: '#/components/schemas/AssistantsApiToolChoiceOption' + nullable: true + parallel_tool_calls: + $ref: '#/components/schemas/ParallelToolCalls' + response_format: + $ref: '#/components/schemas/AssistantsApiResponseFormatOption' + nullable: true + required: + - thread_id + - assistant_id + ListRunsResponse: + type: object + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: '#/components/schemas/RunObject' + first_id: + type: string + example: run_abc123 + last_id: + type: string + example: run_abc456 + has_more: + type: boolean + example: false + required: + - object + - data + - first_id + - last_id + - has_more + ModifyRunRequest: + type: object + additionalProperties: false + properties: + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + SubmitToolOutputsRunRequest: + type: object + additionalProperties: false + properties: + tool_outputs: + description: A list of tools for which the outputs are being submitted. + type: array + items: + type: object + properties: + tool_call_id: + type: string + description: The ID of the tool call in the `required_action` object within the run object the output is being submitted for. + output: + type: string + description: The output of the tool call to be submitted to continue the run. + stream: + type: boolean + nullable: true + description: | + If `true`, returns a stream of events that happen during the Run as server-sent events, terminating when the Run enters a terminal state with a `data: [DONE]` message. + required: + - tool_outputs + RunToolCallObject: + type: object + description: Tool call objects + properties: + id: + type: string + description: The ID of the tool call. This ID must be referenced when you submit the tool outputs in using the [Submit tool outputs to run](/docs/api-reference/runs/submitToolOutputs) endpoint. + type: + type: string + description: The type of tool call the output is required for. For now, this is always `function`. + enum: + - function + function: + type: object + description: The function definition. + properties: + name: + type: string + description: The name of the function. + arguments: + type: string + description: The arguments that the model expects you to pass to the function. + required: + - name + - arguments + required: + - id + - type + - function + CreateThreadAndRunRequest: + type: object + additionalProperties: false + properties: + assistant_id: + description: The ID of the [assistant](/docs/api-reference/assistants) to use to execute this run. + type: string + thread: + $ref: '#/components/schemas/CreateThreadRequest' + description: If no thread is provided, an empty thread will be created. + model: + description: The ID of the [Model](/docs/api-reference/models) to be used to execute this run. If a value is provided here, it will override the model associated with the assistant. If not, the model associated with the assistant will be used. + example: gpt-4o + anyOf: + - type: string + - type: string + enum: + - gpt-4o + - gpt-4o-2024-08-06 + - gpt-4o-2024-05-13 + - gpt-4o-2024-08-06 + - gpt-4o-mini + - gpt-4o-mini-2024-07-18 + - gpt-4-turbo + - gpt-4-turbo-2024-04-09 + - gpt-4-0125-preview + - gpt-4-turbo-preview + - gpt-4-1106-preview + - gpt-4-vision-preview + - gpt-4 + - gpt-4-0314 + - gpt-4-0613 + - gpt-4-32k + - gpt-4-32k-0314 + - gpt-4-32k-0613 + - gpt-3.5-turbo + - gpt-3.5-turbo-16k + - gpt-3.5-turbo-0613 + - gpt-3.5-turbo-1106 + - gpt-3.5-turbo-0125 + - gpt-3.5-turbo-16k-0613 + x-oaiTypeLabel: string + nullable: true + instructions: + description: Override the default system message of the assistant. This is useful for modifying the behavior on a per-run basis. + type: string + nullable: true + tools: + description: Override the tools the assistant can use for this run. This is useful for modifying the behavior on a per-run basis. + nullable: true + type: array + maxItems: 20 + items: + oneOf: + - $ref: '#/components/schemas/AssistantToolsCode' + - $ref: '#/components/schemas/AssistantToolsFileSearch' + - $ref: '#/components/schemas/AssistantToolsFunction' + tool_resources: + type: object + description: | + A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs. + properties: + code_interpreter: + type: object + properties: + file_ids: + type: array + description: | + A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + default: [] + maxItems: 20 + items: + type: string + file_search: + type: object + properties: + vector_store_ids: + type: array + description: | + The ID of the [vector store](/docs/api-reference/vector-stores/object) attached to this assistant. There can be a maximum of 1 vector store attached to the assistant. + maxItems: 1 + items: + type: string + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + temperature: + type: number + minimum: 0 + maximum: 2 + default: 1 + example: 1 + nullable: true + description: | + What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + top_p: + type: number + minimum: 0 + maximum: 1 + default: 1 + example: 1 + nullable: true + description: | + An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. + + We generally recommend altering this or temperature but not both. + stream: + type: boolean + nullable: true + description: | + If `true`, returns a stream of events that happen during the Run as server-sent events, terminating when the Run enters a terminal state with a `data: [DONE]` message. + max_prompt_tokens: + type: integer + nullable: true + description: | + The maximum number of prompt tokens that may be used over the course of the run. The run will make a best effort to use only the number of prompt tokens specified, across multiple turns of the run. If the run exceeds the number of prompt tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info. + minimum: 256 + max_completion_tokens: + type: integer + nullable: true + description: | + The maximum number of completion tokens that may be used over the course of the run. The run will make a best effort to use only the number of completion tokens specified, across multiple turns of the run. If the run exceeds the number of completion tokens specified, the run will end with status `incomplete`. See `incomplete_details` for more info. + minimum: 256 + truncation_strategy: + $ref: '#/components/schemas/TruncationObject' + nullable: true + tool_choice: + $ref: '#/components/schemas/AssistantsApiToolChoiceOption' + nullable: true + parallel_tool_calls: + $ref: '#/components/schemas/ParallelToolCalls' + response_format: + $ref: '#/components/schemas/AssistantsApiResponseFormatOption' + nullable: true + required: + - thread_id + - assistant_id + ThreadObject: + type: object + title: Thread + description: Represents a thread that contains [messages](/docs/api-reference/messages). + properties: + id: + description: The identifier, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `thread`. + type: string + enum: + - thread + created_at: + description: The Unix timestamp (in seconds) for when the thread was created. + type: integer + tool_resources: + type: object + description: | + A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs. + properties: + code_interpreter: + type: object + properties: + file_ids: + type: array + description: | + A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + default: [] + maxItems: 20 + items: + type: string + file_search: + type: object + properties: + vector_store_ids: + type: array + description: | + The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread. + maxItems: 1 + items: + type: string + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + required: + - id + - object + - created_at + - tool_resources + - metadata + x-oaiMeta: + name: The thread object + beta: true + example: | + { + "id": "thread_abc123", + "object": "thread", + "created_at": 1698107661, + "metadata": {} + } + CreateThreadRequest: + type: object + additionalProperties: false + properties: + messages: + description: A list of [messages](/docs/api-reference/messages) to start the thread with. + type: array + items: + $ref: '#/components/schemas/CreateMessageRequest' + tool_resources: + type: object + description: | + A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs. + properties: + code_interpreter: + type: object + properties: + file_ids: + type: array + description: | + A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + default: [] + maxItems: 20 + items: + type: string + file_search: + type: object + properties: + vector_store_ids: + type: array + description: | + The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread. + maxItems: 1 + items: + type: string + vector_stores: + type: array + description: | + A helper to create a [vector store](/docs/api-reference/vector-stores/object) with file_ids and attach it to this thread. There can be a maximum of 1 vector store attached to the thread. + maxItems: 1 + items: + type: object + properties: + file_ids: + type: array + description: | + A list of [file](/docs/api-reference/files) IDs to add to the vector store. There can be a maximum of 10000 files in a vector store. + maxItems: 10000 + items: + type: string + chunking_strategy: + type: object + description: The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy. + oneOf: + - type: object + title: Auto Chunking Strategy + description: The default strategy. This strategy currently uses a `max_chunk_size_tokens` of `800` and `chunk_overlap_tokens` of `400`. + additionalProperties: false + properties: + type: + type: string + description: Always `auto`. + enum: + - auto + required: + - type + - type: object + title: Static Chunking Strategy + additionalProperties: false + properties: + type: + type: string + description: Always `static`. + enum: + - static + static: + type: object + additionalProperties: false + properties: + max_chunk_size_tokens: + type: integer + minimum: 100 + maximum: 4096 + description: The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`. + chunk_overlap_tokens: + type: integer + description: | + The number of tokens that overlap between chunks. The default value is `400`. + + Note that the overlap must not exceed half of `max_chunk_size_tokens`. + required: + - max_chunk_size_tokens + - chunk_overlap_tokens + required: + - type + - static + x-oaiExpandable: true + metadata: + type: object + description: | + Set of 16 key-value pairs that can be attached to a vector store. This can be useful for storing additional information about the vector store in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + x-oaiTypeLabel: map + x-oaiExpandable: true + oneOf: + - required: + - vector_store_ids + - required: + - vector_stores + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + ModifyThreadRequest: + type: object + additionalProperties: false + properties: + tool_resources: + type: object + description: | + A set of resources that are made available to the assistant's tools in this thread. The resources are specific to the type of tool. For example, the `code_interpreter` tool requires a list of file IDs, while the `file_search` tool requires a list of vector store IDs. + properties: + code_interpreter: + type: object + properties: + file_ids: + type: array + description: | + A list of [file](/docs/api-reference/files) IDs made available to the `code_interpreter` tool. There can be a maximum of 20 files associated with the tool. + default: [] + maxItems: 20 + items: + type: string + file_search: + type: object + properties: + vector_store_ids: + type: array + description: | + The [vector store](/docs/api-reference/vector-stores/object) attached to this thread. There can be a maximum of 1 vector store attached to the thread. + maxItems: 1 + items: + type: string + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + DeleteThreadResponse: + type: object + properties: + id: + type: string + deleted: + type: boolean + object: + type: string + enum: + - thread.deleted + required: + - id + - object + - deleted + ListThreadsResponse: + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: '#/components/schemas/ThreadObject' + first_id: + type: string + example: asst_abc123 + last_id: + type: string + example: asst_abc456 + has_more: + type: boolean + example: false + required: + - object + - data + - first_id + - last_id + - has_more + MessageObject: + type: object + title: The message object + description: Represents a message within a [thread](/docs/api-reference/threads). + properties: + id: + description: The identifier, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `thread.message`. + type: string + enum: + - thread.message + created_at: + description: The Unix timestamp (in seconds) for when the message was created. + type: integer + thread_id: + description: The [thread](/docs/api-reference/threads) ID that this message belongs to. + type: string + status: + description: The status of the message, which can be either `in_progress`, `incomplete`, or `completed`. + type: string + enum: + - in_progress + - incomplete + - completed + incomplete_details: + description: On an incomplete message, details about why the message is incomplete. + type: object + properties: + reason: + type: string + description: The reason the message is incomplete. + enum: + - content_filter + - max_tokens + - run_cancelled + - run_expired + - run_failed + nullable: true + required: + - reason + completed_at: + description: The Unix timestamp (in seconds) for when the message was completed. + type: integer + nullable: true + incomplete_at: + description: The Unix timestamp (in seconds) for when the message was marked as incomplete. + type: integer + nullable: true + role: + description: The entity that produced the message. One of `user` or `assistant`. + type: string + enum: + - user + - assistant + content: + description: The content of the message in array of text and/or images. + type: array + items: + oneOf: + - $ref: '#/components/schemas/MessageContentImageFileObject' + - $ref: '#/components/schemas/MessageContentImageUrlObject' + - $ref: '#/components/schemas/MessageContentTextObject' + - $ref: '#/components/schemas/MessageContentRefusalObject' + x-oaiExpandable: true + assistant_id: + description: If applicable, the ID of the [assistant](/docs/api-reference/assistants) that authored this message. + type: string + nullable: true + run_id: + description: The ID of the [run](/docs/api-reference/runs) associated with the creation of this message. Value is `null` when messages are created manually using the create message or create thread endpoints. + type: string + nullable: true + attachments: + type: array + items: + type: object + properties: + file_id: + type: string + description: The ID of the file to attach to the message. + tools: + description: The tools to add this file to. + type: array + items: + oneOf: + - $ref: '#/components/schemas/AssistantToolsCode' + - $ref: '#/components/schemas/AssistantToolsFileSearchTypeOnly' + x-oaiExpandable: true + description: A list of files attached to the message, and the tools they were added to. + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + required: + - id + - object + - created_at + - thread_id + - status + - incomplete_details + - completed_at + - incomplete_at + - role + - content + - assistant_id + - run_id + - attachments + - metadata + x-oaiMeta: + name: The message object + beta: true + example: | + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1698983503, + "thread_id": "thread_abc123", + "role": "assistant", + "content": [ + { + "type": "text", + "text": { + "value": "Hi! How can I help you today?", + "annotations": [] + } + } + ], + "assistant_id": "asst_abc123", + "run_id": "run_abc123", + "attachments": [], + "metadata": {} + } + MessageDeltaObject: + type: object + title: Message delta object + description: | + Represents a message delta i.e. any changed fields on a message during streaming. + properties: + id: + description: The identifier of the message, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `thread.message.delta`. + type: string + enum: + - thread.message.delta + delta: + description: The delta containing the fields that have changed on the Message. + type: object + properties: + role: + description: The entity that produced the message. One of `user` or `assistant`. + type: string + enum: + - user + - assistant + content: + description: The content of the message in array of text and/or images. + type: array + items: + oneOf: + - $ref: '#/components/schemas/MessageDeltaContentImageFileObject' + - $ref: '#/components/schemas/MessageDeltaContentTextObject' + - $ref: '#/components/schemas/MessageDeltaContentRefusalObject' + - $ref: '#/components/schemas/MessageDeltaContentImageUrlObject' + x-oaiExpandable: true + required: + - id + - object + - delta + x-oaiMeta: + name: The message delta object + beta: true + example: | + { + "id": "msg_123", + "object": "thread.message.delta", + "delta": { + "content": [ + { + "index": 0, + "type": "text", + "text": { "value": "Hello", "annotations": [] } + } + ] + } + } + CreateMessageRequest: + type: object + additionalProperties: false + required: + - role + - content + properties: + role: + type: string + enum: + - user + - assistant + description: | + The role of the entity that is creating the message. Allowed values include: + - `user`: Indicates the message is sent by an actual user and should be used in most cases to represent user-generated messages. + - `assistant`: Indicates the message is generated by the assistant. Use this value to insert messages from the assistant into the conversation. + content: + oneOf: + - type: string + description: The text contents of the message. + title: Text content + - type: array + description: An array of content parts with a defined type, each can be of type `text` or images can be passed with `image_url` or `image_file`. Image types are only supported on [Vision-compatible models](/docs/models/overview). + title: Array of content parts + items: + oneOf: + - $ref: '#/components/schemas/MessageContentImageFileObject' + - $ref: '#/components/schemas/MessageContentImageUrlObject' + - $ref: '#/components/schemas/MessageRequestContentTextObject' + x-oaiExpandable: true + minItems: 1 + x-oaiExpandable: true + attachments: + type: array + items: + type: object + properties: + file_id: + type: string + description: The ID of the file to attach to the message. + tools: + description: The tools to add this file to. + type: array + items: + oneOf: + - $ref: '#/components/schemas/AssistantToolsCode' + - $ref: '#/components/schemas/AssistantToolsFileSearchTypeOnly' + x-oaiExpandable: true + description: A list of files attached to the message, and the tools they should be added to. + required: + - file_id + - tools + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + ModifyMessageRequest: + type: object + additionalProperties: false + properties: + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + DeleteMessageResponse: + type: object + properties: + id: + type: string + deleted: + type: boolean + object: + type: string + enum: + - thread.message.deleted + required: + - id + - object + - deleted + ListMessagesResponse: + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: '#/components/schemas/MessageObject' + first_id: + type: string + example: msg_abc123 + last_id: + type: string + example: msg_abc123 + has_more: + type: boolean + example: false + required: + - object + - data + - first_id + - last_id + - has_more + MessageContentImageFileObject: + title: Image file + type: object + description: References an image [File](/docs/api-reference/files) in the content of a message. + properties: + type: + description: Always `image_file`. + type: string + enum: + - image_file + image_file: + type: object + properties: + file_id: + description: The [File](/docs/api-reference/files) ID of the image in the message content. Set `purpose="vision"` when uploading the File if you need to later display the file content. + type: string + detail: + type: string + description: Specifies the detail level of the image if specified by the user. `low` uses fewer tokens, you can opt in to high resolution using `high`. + enum: + - auto + - low + - high + default: auto + required: + - file_id + required: + - type + - image_file + MessageDeltaContentImageFileObject: + title: Image file + type: object + description: References an image [File](/docs/api-reference/files) in the content of a message. + properties: + index: + type: integer + description: The index of the content part in the message. + type: + description: Always `image_file`. + type: string + enum: + - image_file + image_file: + type: object + properties: + file_id: + description: The [File](/docs/api-reference/files) ID of the image in the message content. Set `purpose="vision"` when uploading the File if you need to later display the file content. + type: string + detail: + type: string + description: Specifies the detail level of the image if specified by the user. `low` uses fewer tokens, you can opt in to high resolution using `high`. + enum: + - auto + - low + - high + default: auto + required: + - index + - type + MessageContentImageUrlObject: + title: Image URL + type: object + description: References an image URL in the content of a message. + properties: + type: + type: string + enum: + - image_url + description: The type of the content part. + image_url: + type: object + properties: + url: + type: string + description: 'The external URL of the image, must be a supported image types: jpeg, jpg, png, gif, webp.' + format: uri + detail: + type: string + description: Specifies the detail level of the image. `low` uses fewer tokens, you can opt in to high resolution using `high`. Default value is `auto` + enum: + - auto + - low + - high + default: auto + required: + - url + required: + - type + - image_url + MessageDeltaContentImageUrlObject: + title: Image URL + type: object + description: References an image URL in the content of a message. + properties: + index: + type: integer + description: The index of the content part in the message. + type: + description: Always `image_url`. + type: string + enum: + - image_url + image_url: + type: object + properties: + url: + description: 'The URL of the image, must be a supported image types: jpeg, jpg, png, gif, webp.' + type: string + detail: + type: string + description: Specifies the detail level of the image. `low` uses fewer tokens, you can opt in to high resolution using `high`. + enum: + - auto + - low + - high + default: auto + required: + - index + - type + MessageContentTextObject: + title: Text + type: object + description: The text content that is part of a message. + properties: + type: + description: Always `text`. + type: string + enum: + - text + text: + type: object + properties: + value: + description: The data that makes up the text. + type: string + annotations: + type: array + items: + oneOf: + - $ref: '#/components/schemas/MessageContentTextAnnotationsFileCitationObject' + - $ref: '#/components/schemas/MessageContentTextAnnotationsFilePathObject' + x-oaiExpandable: true + required: + - value + - annotations + required: + - type + - text + MessageContentRefusalObject: + title: Refusal + type: object + description: The refusal content generated by the assistant. + properties: + type: + description: Always `refusal`. + type: string + enum: + - refusal + refusal: + type: string + nullable: false + required: + - type + - refusal + MessageRequestContentTextObject: + title: Text + type: object + description: The text content that is part of a message. + properties: + type: + description: Always `text`. + type: string + enum: + - text + text: + type: string + description: Text content to be sent to the model + required: + - type + - text + MessageContentTextAnnotationsFileCitationObject: + title: File citation + type: object + description: A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the "file_search" tool to search files. + properties: + type: + description: Always `file_citation`. + type: string + enum: + - file_citation + text: + description: The text in the message content that needs to be replaced. + type: string + file_citation: + type: object + properties: + file_id: + description: The ID of the specific File the citation is from. + type: string + required: + - file_id + start_index: + type: integer + minimum: 0 + end_index: + type: integer + minimum: 0 + required: + - type + - text + - file_citation + - start_index + - end_index + MessageContentTextAnnotationsFilePathObject: + title: File path + type: object + description: A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file. + properties: + type: + description: Always `file_path`. + type: string + enum: + - file_path + text: + description: The text in the message content that needs to be replaced. + type: string + file_path: + type: object + properties: + file_id: + description: The ID of the file that was generated. + type: string + required: + - file_id + start_index: + type: integer + minimum: 0 + end_index: + type: integer + minimum: 0 + required: + - type + - text + - file_path + - start_index + - end_index + MessageDeltaContentTextObject: + title: Text + type: object + description: The text content that is part of a message. + properties: + index: + type: integer + description: The index of the content part in the message. + type: + description: Always `text`. + type: string + enum: + - text + text: + type: object + properties: + value: + description: The data that makes up the text. + type: string + annotations: + type: array + items: + oneOf: + - $ref: '#/components/schemas/MessageDeltaContentTextAnnotationsFileCitationObject' + - $ref: '#/components/schemas/MessageDeltaContentTextAnnotationsFilePathObject' + x-oaiExpandable: true + required: + - index + - type + MessageDeltaContentRefusalObject: + title: Refusal + type: object + description: The refusal content that is part of a message. + properties: + index: + type: integer + description: The index of the refusal part in the message. + type: + description: Always `refusal`. + type: string + enum: + - refusal + refusal: + type: string + required: + - index + - type + MessageDeltaContentTextAnnotationsFileCitationObject: + title: File citation + type: object + description: A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the "file_search" tool to search files. + properties: + index: + type: integer + description: The index of the annotation in the text content part. + type: + description: Always `file_citation`. + type: string + enum: + - file_citation + text: + description: The text in the message content that needs to be replaced. + type: string + file_citation: + type: object + properties: + file_id: + description: The ID of the specific File the citation is from. + type: string + quote: + description: The specific quote in the file. + type: string + start_index: + type: integer + minimum: 0 + end_index: + type: integer + minimum: 0 + required: + - index + - type + MessageDeltaContentTextAnnotationsFilePathObject: + title: File path + type: object + description: A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file. + properties: + index: + type: integer + description: The index of the annotation in the text content part. + type: + description: Always `file_path`. + type: string + enum: + - file_path + text: + description: The text in the message content that needs to be replaced. + type: string + file_path: + type: object + properties: + file_id: + description: The ID of the file that was generated. + type: string + start_index: + type: integer + minimum: 0 + end_index: + type: integer + minimum: 0 + required: + - index + - type + RunStepObject: + type: object + title: Run steps + description: | + Represents a step in execution of a run. + properties: + id: + description: The identifier of the run step, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `thread.run.step`. + type: string + enum: + - thread.run.step + created_at: + description: The Unix timestamp (in seconds) for when the run step was created. + type: integer + assistant_id: + description: The ID of the [assistant](/docs/api-reference/assistants) associated with the run step. + type: string + thread_id: + description: The ID of the [thread](/docs/api-reference/threads) that was run. + type: string + run_id: + description: The ID of the [run](/docs/api-reference/runs) that this run step is a part of. + type: string + type: + description: The type of run step, which can be either `message_creation` or `tool_calls`. + type: string + enum: + - message_creation + - tool_calls + status: + description: The status of the run step, which can be either `in_progress`, `cancelled`, `failed`, `completed`, or `expired`. + type: string + enum: + - in_progress + - cancelled + - failed + - completed + - expired + step_details: + type: object + description: The details of the run step. + oneOf: + - $ref: '#/components/schemas/RunStepDetailsMessageCreationObject' + - $ref: '#/components/schemas/RunStepDetailsToolCallsObject' + x-oaiExpandable: true + last_error: + type: object + description: The last error associated with this run step. Will be `null` if there are no errors. + nullable: true + properties: + code: + type: string + description: One of `server_error` or `rate_limit_exceeded`. + enum: + - server_error + - rate_limit_exceeded + message: + type: string + description: A human-readable description of the error. + required: + - code + - message + expired_at: + description: The Unix timestamp (in seconds) for when the run step expired. A step is considered expired if the parent run is expired. + type: integer + nullable: true + cancelled_at: + description: The Unix timestamp (in seconds) for when the run step was cancelled. + type: integer + nullable: true + failed_at: + description: The Unix timestamp (in seconds) for when the run step failed. + type: integer + nullable: true + completed_at: + description: The Unix timestamp (in seconds) for when the run step completed. + type: integer + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + usage: + $ref: '#/components/schemas/RunStepCompletionUsage' + required: + - id + - object + - created_at + - assistant_id + - thread_id + - run_id + - type + - status + - step_details + - last_error + - expired_at + - cancelled_at + - failed_at + - completed_at + - metadata + - usage + x-oaiMeta: + name: The run step object + beta: true + example: | + { + "id": "step_abc123", + "object": "thread.run.step", + "created_at": 1699063291, + "run_id": "run_abc123", + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "type": "message_creation", + "status": "completed", + "cancelled_at": null, + "completed_at": 1699063291, + "expired_at": null, + "failed_at": null, + "last_error": null, + "step_details": { + "type": "message_creation", + "message_creation": { + "message_id": "msg_abc123" + } + }, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + } + } + RunStepDeltaObject: + type: object + title: Run step delta object + description: | + Represents a run step delta i.e. any changed fields on a run step during streaming. + properties: + id: + description: The identifier of the run step, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `thread.run.step.delta`. + type: string + enum: + - thread.run.step.delta + delta: + description: The delta containing the fields that have changed on the run step. + type: object + properties: + step_details: + type: object + description: The details of the run step. + oneOf: + - $ref: '#/components/schemas/RunStepDeltaStepDetailsMessageCreationObject' + - $ref: '#/components/schemas/RunStepDeltaStepDetailsToolCallsObject' + x-oaiExpandable: true + required: + - id + - object + - delta + x-oaiMeta: + name: The run step delta object + beta: true + example: | + { + "id": "step_123", + "object": "thread.run.step.delta", + "delta": { + "step_details": { + "type": "tool_calls", + "tool_calls": [ + { + "index": 0, + "id": "call_123", + "type": "code_interpreter", + "code_interpreter": { "input": "", "outputs": [] } + } + ] + } + } + } + ListRunStepsResponse: + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: '#/components/schemas/RunStepObject' + first_id: + type: string + example: step_abc123 + last_id: + type: string + example: step_abc456 + has_more: + type: boolean + example: false + required: + - object + - data + - first_id + - last_id + - has_more + RunStepDetailsMessageCreationObject: + title: Message creation + type: object + description: Details of the message creation by the run step. + properties: + type: + description: Always `message_creation`. + type: string + enum: + - message_creation + message_creation: + type: object + properties: + message_id: + type: string + description: The ID of the message that was created by this run step. + required: + - message_id + required: + - type + - message_creation + RunStepDeltaStepDetailsMessageCreationObject: + title: Message creation + type: object + description: Details of the message creation by the run step. + properties: + type: + description: Always `message_creation`. + type: string + enum: + - message_creation + message_creation: + type: object + properties: + message_id: + type: string + description: The ID of the message that was created by this run step. + required: + - type + RunStepDetailsToolCallsObject: + title: Tool calls + type: object + description: Details of the tool call. + properties: + type: + description: Always `tool_calls`. + type: string + enum: + - tool_calls + tool_calls: + type: array + description: | + An array of tool calls the run step was involved in. These can be associated with one of three types of tools: `code_interpreter`, `file_search`, or `function`. + items: + oneOf: + - $ref: '#/components/schemas/RunStepDetailsToolCallsCodeObject' + - $ref: '#/components/schemas/RunStepDetailsToolCallsFileSearchObject' + - $ref: '#/components/schemas/RunStepDetailsToolCallsFunctionObject' + x-oaiExpandable: true + required: + - type + - tool_calls + RunStepDeltaStepDetailsToolCallsObject: + title: Tool calls + type: object + description: Details of the tool call. + properties: + type: + description: Always `tool_calls`. + type: string + enum: + - tool_calls + tool_calls: + type: array + description: | + An array of tool calls the run step was involved in. These can be associated with one of three types of tools: `code_interpreter`, `file_search`, or `function`. + items: + oneOf: + - $ref: '#/components/schemas/RunStepDeltaStepDetailsToolCallsCodeObject' + - $ref: '#/components/schemas/RunStepDeltaStepDetailsToolCallsFileSearchObject' + - $ref: '#/components/schemas/RunStepDeltaStepDetailsToolCallsFunctionObject' + x-oaiExpandable: true + required: + - type + RunStepDetailsToolCallsCodeObject: + title: Code Interpreter tool call + type: object + description: Details of the Code Interpreter tool call the run step was involved in. + properties: + id: + type: string + description: The ID of the tool call. + type: + type: string + description: The type of tool call. This is always going to be `code_interpreter` for this type of tool call. + enum: + - code_interpreter + code_interpreter: + type: object + description: The Code Interpreter tool call definition. + required: + - input + - outputs + properties: + input: + type: string + description: The input to the Code Interpreter tool call. + outputs: + type: array + description: The outputs from the Code Interpreter tool call. Code Interpreter can output one or more items, including text (`logs`) or images (`image`). Each of these are represented by a different object type. + items: + type: object + oneOf: + - $ref: '#/components/schemas/RunStepDetailsToolCallsCodeOutputLogsObject' + - $ref: '#/components/schemas/RunStepDetailsToolCallsCodeOutputImageObject' + x-oaiExpandable: true + required: + - id + - type + - code_interpreter + RunStepDeltaStepDetailsToolCallsCodeObject: + title: Code interpreter tool call + type: object + description: Details of the Code Interpreter tool call the run step was involved in. + properties: + index: + type: integer + description: The index of the tool call in the tool calls array. + id: + type: string + description: The ID of the tool call. + type: + type: string + description: The type of tool call. This is always going to be `code_interpreter` for this type of tool call. + enum: + - code_interpreter + code_interpreter: + type: object + description: The Code Interpreter tool call definition. + properties: + input: + type: string + description: The input to the Code Interpreter tool call. + outputs: + type: array + description: The outputs from the Code Interpreter tool call. Code Interpreter can output one or more items, including text (`logs`) or images (`image`). Each of these are represented by a different object type. + items: + type: object + oneOf: + - $ref: '#/components/schemas/RunStepDeltaStepDetailsToolCallsCodeOutputLogsObject' + - $ref: '#/components/schemas/RunStepDeltaStepDetailsToolCallsCodeOutputImageObject' + x-oaiExpandable: true + required: + - index + - type + RunStepDetailsToolCallsCodeOutputLogsObject: + title: Code Interpreter log output + type: object + description: Text output from the Code Interpreter tool call as part of a run step. + properties: + type: + description: Always `logs`. + type: string + enum: + - logs + logs: + type: string + description: The text output from the Code Interpreter tool call. + required: + - type + - logs + RunStepDeltaStepDetailsToolCallsCodeOutputLogsObject: + title: Code interpreter log output + type: object + description: Text output from the Code Interpreter tool call as part of a run step. + properties: + index: + type: integer + description: The index of the output in the outputs array. + type: + description: Always `logs`. + type: string + enum: + - logs + logs: + type: string + description: The text output from the Code Interpreter tool call. + required: + - index + - type + RunStepDetailsToolCallsCodeOutputImageObject: + title: Code Interpreter image output + type: object + properties: + type: + description: Always `image`. + type: string + enum: + - image + image: + type: object + properties: + file_id: + description: The [file](/docs/api-reference/files) ID of the image. + type: string + required: + - file_id + required: + - type + - image + RunStepDeltaStepDetailsToolCallsCodeOutputImageObject: + title: Code interpreter image output + type: object + properties: + index: + type: integer + description: The index of the output in the outputs array. + type: + description: Always `image`. + type: string + enum: + - image + image: + type: object + properties: + file_id: + description: The [file](/docs/api-reference/files) ID of the image. + type: string + required: + - index + - type + RunStepDetailsToolCallsFileSearchObject: + title: File search tool call + type: object + properties: + id: + type: string + description: The ID of the tool call object. + type: + type: string + description: The type of tool call. This is always going to be `file_search` for this type of tool call. + enum: + - file_search + file_search: + type: object + description: For now, this is always going to be an empty object. + x-oaiTypeLabel: map + required: + - id + - type + - file_search + RunStepDeltaStepDetailsToolCallsFileSearchObject: + title: File search tool call + type: object + properties: + index: + type: integer + description: The index of the tool call in the tool calls array. + id: + type: string + description: The ID of the tool call object. + type: + type: string + description: The type of tool call. This is always going to be `file_search` for this type of tool call. + enum: + - file_search + file_search: + type: object + description: For now, this is always going to be an empty object. + x-oaiTypeLabel: map + required: + - index + - type + - file_search + RunStepDetailsToolCallsFunctionObject: + type: object + title: Function tool call + properties: + id: + type: string + description: The ID of the tool call object. + type: + type: string + description: The type of tool call. This is always going to be `function` for this type of tool call. + enum: + - function + function: + type: object + description: The definition of the function that was called. + properties: + name: + type: string + description: The name of the function. + arguments: + type: string + description: The arguments passed to the function. + output: + type: string + description: The output of the function. This will be `null` if the outputs have not been [submitted](/docs/api-reference/runs/submitToolOutputs) yet. + nullable: true + required: + - name + - arguments + - output + required: + - id + - type + - function + RunStepDeltaStepDetailsToolCallsFunctionObject: + type: object + title: Function tool call + properties: + index: + type: integer + description: The index of the tool call in the tool calls array. + id: + type: string + description: The ID of the tool call object. + type: + type: string + description: The type of tool call. This is always going to be `function` for this type of tool call. + enum: + - function + function: + type: object + description: The definition of the function that was called. + properties: + name: + type: string + description: The name of the function. + arguments: + type: string + description: The arguments passed to the function. + output: + type: string + description: The output of the function. This will be `null` if the outputs have not been [submitted](/docs/api-reference/runs/submitToolOutputs) yet. + nullable: true + required: + - index + - type + VectorStoreExpirationAfter: + type: object + title: Vector store expiration policy + description: The expiration policy for a vector store. + properties: + anchor: + description: 'Anchor timestamp after which the expiration policy applies. Supported anchors: `last_active_at`.' + type: string + enum: + - last_active_at + days: + description: The number of days after the anchor time that the vector store will expire. + type: integer + minimum: 1 + maximum: 365 + required: + - anchor + - days + VectorStoreObject: + type: object + title: Vector store + description: A vector store is a collection of processed files can be used by the `file_search` tool. + properties: + id: + description: The identifier, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `vector_store`. + type: string + enum: + - vector_store + created_at: + description: The Unix timestamp (in seconds) for when the vector store was created. + type: integer + name: + description: The name of the vector store. + type: string + usage_bytes: + description: The total number of bytes used by the files in the vector store. + type: integer + file_counts: + type: object + properties: + in_progress: + description: The number of files that are currently being processed. + type: integer + completed: + description: The number of files that have been successfully processed. + type: integer + failed: + description: The number of files that have failed to process. + type: integer + cancelled: + description: The number of files that were cancelled. + type: integer + total: + description: The total number of files. + type: integer + required: + - in_progress + - completed + - failed + - cancelled + - total + status: + description: The status of the vector store, which can be either `expired`, `in_progress`, or `completed`. A status of `completed` indicates that the vector store is ready for use. + type: string + enum: + - expired + - in_progress + - completed + expires_after: + $ref: '#/components/schemas/VectorStoreExpirationAfter' + expires_at: + description: The Unix timestamp (in seconds) for when the vector store will expire. + type: integer + nullable: true + last_active_at: + description: The Unix timestamp (in seconds) for when the vector store was last active. + type: integer + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + required: + - id + - object + - usage_bytes + - created_at + - status + - last_active_at + - name + - file_counts + - metadata + x-oaiMeta: + name: The vector store object + beta: true + example: | + { + "id": "vs_123", + "object": "vector_store", + "created_at": 1698107661, + "usage_bytes": 123456, + "last_active_at": 1698107661, + "name": "my_vector_store", + "status": "completed", + "file_counts": { + "in_progress": 0, + "completed": 100, + "cancelled": 0, + "failed": 0, + "total": 100 + }, + "metadata": {}, + "last_used_at": 1698107661 + } + CreateVectorStoreRequest: + type: object + additionalProperties: false + properties: + file_ids: + description: A list of [File](/docs/api-reference/files) IDs that the vector store should use. Useful for tools like `file_search` that can access files. + type: array + maxItems: 500 + items: + type: string + name: + description: The name of the vector store. + type: string + expires_after: + $ref: '#/components/schemas/VectorStoreExpirationAfter' + chunking_strategy: + type: object + description: The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy. Only applicable if `file_ids` is non-empty. + oneOf: + - $ref: '#/components/schemas/AutoChunkingStrategyRequestParam' + - $ref: '#/components/schemas/StaticChunkingStrategyRequestParam' + x-oaiExpandable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + UpdateVectorStoreRequest: + type: object + additionalProperties: false + properties: + name: + description: The name of the vector store. + type: string + nullable: true + expires_after: + $ref: '#/components/schemas/VectorStoreExpirationAfter' + nullable: true + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + ListVectorStoresResponse: + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: '#/components/schemas/VectorStoreObject' + first_id: + type: string + example: vs_abc123 + last_id: + type: string + example: vs_abc456 + has_more: + type: boolean + example: false + required: + - object + - data + - first_id + - last_id + - has_more + DeleteVectorStoreResponse: + type: object + properties: + id: + type: string + deleted: + type: boolean + object: + type: string + enum: + - vector_store.deleted + required: + - id + - object + - deleted + VectorStoreFileObject: + type: object + title: Vector store files + description: A list of files attached to a vector store. + properties: + id: + description: The identifier, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `vector_store.file`. + type: string + enum: + - vector_store.file + usage_bytes: + description: The total vector store usage in bytes. Note that this may be different from the original file size. + type: integer + created_at: + description: The Unix timestamp (in seconds) for when the vector store file was created. + type: integer + vector_store_id: + description: The ID of the [vector store](/docs/api-reference/vector-stores/object) that the [File](/docs/api-reference/files) is attached to. + type: string + status: + description: The status of the vector store file, which can be either `in_progress`, `completed`, `cancelled`, or `failed`. The status `completed` indicates that the vector store file is ready for use. + type: string + enum: + - in_progress + - completed + - cancelled + - failed + last_error: + type: object + description: The last error associated with this vector store file. Will be `null` if there are no errors. + nullable: true + properties: + code: + type: string + description: One of `server_error` or `rate_limit_exceeded`. + enum: + - server_error + - unsupported_file + - invalid_file + message: + type: string + description: A human-readable description of the error. + required: + - code + - message + chunking_strategy: + type: object + description: The strategy used to chunk the file. + oneOf: + - $ref: '#/components/schemas/StaticChunkingStrategyResponseParam' + - $ref: '#/components/schemas/OtherChunkingStrategyResponseParam' + x-oaiExpandable: true + required: + - id + - object + - usage_bytes + - created_at + - vector_store_id + - status + - last_error + x-oaiMeta: + name: The vector store file object + beta: true + example: | + { + "id": "file-abc123", + "object": "vector_store.file", + "usage_bytes": 1234, + "created_at": 1698107661, + "vector_store_id": "vs_abc123", + "status": "completed", + "last_error": null, + "chunking_strategy": { + "type": "static", + "static": { + "max_chunk_size_tokens": 800, + "chunk_overlap_tokens": 400 + } + } + } + OtherChunkingStrategyResponseParam: + type: object + title: Other Chunking Strategy + description: This is returned when the chunking strategy is unknown. Typically, this is because the file was indexed before the `chunking_strategy` concept was introduced in the API. + additionalProperties: false + properties: + type: + type: string + description: Always `other`. + enum: + - other + required: + - type + StaticChunkingStrategyResponseParam: + type: object + title: Static Chunking Strategy + additionalProperties: false + properties: + type: + type: string + description: Always `static`. + enum: + - static + static: + $ref: '#/components/schemas/StaticChunkingStrategy' + required: + - type + - static + StaticChunkingStrategy: + type: object + additionalProperties: false + properties: + max_chunk_size_tokens: + type: integer + minimum: 100 + maximum: 4096 + description: The maximum number of tokens in each chunk. The default value is `800`. The minimum value is `100` and the maximum value is `4096`. + chunk_overlap_tokens: + type: integer + description: | + The number of tokens that overlap between chunks. The default value is `400`. + + Note that the overlap must not exceed half of `max_chunk_size_tokens`. + required: + - max_chunk_size_tokens + - chunk_overlap_tokens + AutoChunkingStrategyRequestParam: + type: object + title: Auto Chunking Strategy + description: The default strategy. This strategy currently uses a `max_chunk_size_tokens` of `800` and `chunk_overlap_tokens` of `400`. + additionalProperties: false + properties: + type: + type: string + description: Always `auto`. + enum: + - auto + required: + - type + StaticChunkingStrategyRequestParam: + type: object + title: Static Chunking Strategy + additionalProperties: false + properties: + type: + type: string + description: Always `static`. + enum: + - static + static: + $ref: '#/components/schemas/StaticChunkingStrategy' + required: + - type + - static + ChunkingStrategyRequestParam: + type: object + description: The chunking strategy used to chunk the file(s). If not set, will use the `auto` strategy. + oneOf: + - $ref: '#/components/schemas/AutoChunkingStrategyRequestParam' + - $ref: '#/components/schemas/StaticChunkingStrategyRequestParam' + x-oaiExpandable: true + CreateVectorStoreFileRequest: + type: object + additionalProperties: false + properties: + file_id: + description: A [File](/docs/api-reference/files) ID that the vector store should use. Useful for tools like `file_search` that can access files. + type: string + chunking_strategy: + $ref: '#/components/schemas/ChunkingStrategyRequestParam' + required: + - file_id + ListVectorStoreFilesResponse: + properties: + object: + type: string + example: list + data: + type: array + items: + $ref: '#/components/schemas/VectorStoreFileObject' + first_id: + type: string + example: file-abc123 + last_id: + type: string + example: file-abc456 + has_more: + type: boolean + example: false + required: + - object + - data + - first_id + - last_id + - has_more + DeleteVectorStoreFileResponse: + type: object + properties: + id: + type: string + deleted: + type: boolean + object: + type: string + enum: + - vector_store.file.deleted + required: + - id + - object + - deleted + VectorStoreFileBatchObject: + type: object + title: Vector store file batch + description: A batch of files attached to a vector store. + properties: + id: + description: The identifier, which can be referenced in API endpoints. + type: string + object: + description: The object type, which is always `vector_store.file_batch`. + type: string + enum: + - vector_store.files_batch + created_at: + description: The Unix timestamp (in seconds) for when the vector store files batch was created. + type: integer + vector_store_id: + description: The ID of the [vector store](/docs/api-reference/vector-stores/object) that the [File](/docs/api-reference/files) is attached to. + type: string + status: + description: The status of the vector store files batch, which can be either `in_progress`, `completed`, `cancelled` or `failed`. + type: string + enum: + - in_progress + - completed + - cancelled + - failed + file_counts: + type: object + properties: + in_progress: + description: The number of files that are currently being processed. + type: integer + completed: + description: The number of files that have been processed. + type: integer + failed: + description: The number of files that have failed to process. + type: integer + cancelled: + description: The number of files that where cancelled. + type: integer + total: + description: The total number of files. + type: integer + required: + - in_progress + - completed + - cancelled + - failed + - total + required: + - id + - object + - created_at + - vector_store_id + - status + - file_counts + x-oaiMeta: + name: The vector store files batch object + beta: true + example: | + { + "id": "vsfb_123", + "object": "vector_store.files_batch", + "created_at": 1698107661, + "vector_store_id": "vs_abc123", + "status": "completed", + "file_counts": { + "in_progress": 0, + "completed": 100, + "failed": 0, + "cancelled": 0, + "total": 100 + } + } + CreateVectorStoreFileBatchRequest: + type: object + additionalProperties: false + properties: + file_ids: + description: A list of [File](/docs/api-reference/files) IDs that the vector store should use. Useful for tools like `file_search` that can access files. + type: array + minItems: 1 + maxItems: 500 + items: + type: string + chunking_strategy: + $ref: '#/components/schemas/ChunkingStrategyRequestParam' + required: + - file_ids + AssistantStreamEvent: + description: | + Represents an event emitted when streaming a Run. + + Each event in a server-sent events stream has an `event` and `data` property: + + ``` + event: thread.created + data: {"id": "thread_123", "object": "thread", ...} + ``` + + We emit events whenever a new object is created, transitions to a new state, or is being + streamed in parts (deltas). For example, we emit `thread.run.created` when a new run + is created, `thread.run.completed` when a run completes, and so on. When an Assistant chooses + to create a message during a run, we emit a `thread.message.created event`, a + `thread.message.in_progress` event, many `thread.message.delta` events, and finally a + `thread.message.completed` event. + + We may add additional events over time, so we recommend handling unknown events gracefully + in your code. See the [Assistants API quickstart](/docs/assistants/overview) to learn how to + integrate the Assistants API with streaming. + oneOf: + - $ref: '#/components/schemas/ThreadStreamEvent' + - $ref: '#/components/schemas/RunStreamEvent' + - $ref: '#/components/schemas/RunStepStreamEvent' + - $ref: '#/components/schemas/MessageStreamEvent' + - $ref: '#/components/schemas/ErrorEvent' + - $ref: '#/components/schemas/DoneEvent' + x-oaiMeta: + name: Assistant stream events + beta: true + ThreadStreamEvent: + oneOf: + - type: object + properties: + event: + type: string + enum: + - thread.created + data: + $ref: '#/components/schemas/ThreadObject' + required: + - event + - data + description: Occurs when a new [thread](/docs/api-reference/threads/object) is created. + x-oaiMeta: + dataDescription: '`data` is a [thread](/docs/api-reference/threads/object)' + RunStreamEvent: + oneOf: + - type: object + properties: + event: + type: string + enum: + - thread.run.created + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a new [run](/docs/api-reference/runs/object) is created. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.queued + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) moves to a `queued` status. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.in_progress + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) moves to an `in_progress` status. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.requires_action + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) moves to a `requires_action` status. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.completed + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) is completed. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.incomplete + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) ends with status `incomplete`. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.failed + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) fails. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.cancelling + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) moves to a `cancelling` status. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.cancelled + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) is cancelled. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.expired + data: + $ref: '#/components/schemas/RunObject' + required: + - event + - data + description: Occurs when a [run](/docs/api-reference/runs/object) expires. + x-oaiMeta: + dataDescription: '`data` is a [run](/docs/api-reference/runs/object)' + RunStepStreamEvent: + oneOf: + - type: object + properties: + event: + type: string + enum: + - thread.run.step.created + data: + $ref: '#/components/schemas/RunStepObject' + required: + - event + - data + description: Occurs when a [run step](/docs/api-reference/runs/step-object) is created. + x-oaiMeta: + dataDescription: '`data` is a [run step](/docs/api-reference/runs/step-object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.step.in_progress + data: + $ref: '#/components/schemas/RunStepObject' + required: + - event + - data + description: Occurs when a [run step](/docs/api-reference/runs/step-object) moves to an `in_progress` state. + x-oaiMeta: + dataDescription: '`data` is a [run step](/docs/api-reference/runs/step-object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.step.delta + data: + $ref: '#/components/schemas/RunStepDeltaObject' + required: + - event + - data + description: Occurs when parts of a [run step](/docs/api-reference/runs/step-object) are being streamed. + x-oaiMeta: + dataDescription: '`data` is a [run step delta](/docs/api-reference/assistants-streaming/run-step-delta-object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.step.completed + data: + $ref: '#/components/schemas/RunStepObject' + required: + - event + - data + description: Occurs when a [run step](/docs/api-reference/runs/step-object) is completed. + x-oaiMeta: + dataDescription: '`data` is a [run step](/docs/api-reference/runs/step-object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.step.failed + data: + $ref: '#/components/schemas/RunStepObject' + required: + - event + - data + description: Occurs when a [run step](/docs/api-reference/runs/step-object) fails. + x-oaiMeta: + dataDescription: '`data` is a [run step](/docs/api-reference/runs/step-object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.step.cancelled + data: + $ref: '#/components/schemas/RunStepObject' + required: + - event + - data + description: Occurs when a [run step](/docs/api-reference/runs/step-object) is cancelled. + x-oaiMeta: + dataDescription: '`data` is a [run step](/docs/api-reference/runs/step-object)' + - type: object + properties: + event: + type: string + enum: + - thread.run.step.expired + data: + $ref: '#/components/schemas/RunStepObject' + required: + - event + - data + description: Occurs when a [run step](/docs/api-reference/runs/step-object) expires. + x-oaiMeta: + dataDescription: '`data` is a [run step](/docs/api-reference/runs/step-object)' + MessageStreamEvent: + oneOf: + - type: object + properties: + event: + type: string + enum: + - thread.message.created + data: + $ref: '#/components/schemas/MessageObject' + required: + - event + - data + description: Occurs when a [message](/docs/api-reference/messages/object) is created. + x-oaiMeta: + dataDescription: '`data` is a [message](/docs/api-reference/messages/object)' + - type: object + properties: + event: + type: string + enum: + - thread.message.in_progress + data: + $ref: '#/components/schemas/MessageObject' + required: + - event + - data + description: Occurs when a [message](/docs/api-reference/messages/object) moves to an `in_progress` state. + x-oaiMeta: + dataDescription: '`data` is a [message](/docs/api-reference/messages/object)' + - type: object + properties: + event: + type: string + enum: + - thread.message.delta + data: + $ref: '#/components/schemas/MessageDeltaObject' + required: + - event + - data + description: Occurs when parts of a [Message](/docs/api-reference/messages/object) are being streamed. + x-oaiMeta: + dataDescription: '`data` is a [message delta](/docs/api-reference/assistants-streaming/message-delta-object)' + - type: object + properties: + event: + type: string + enum: + - thread.message.completed + data: + $ref: '#/components/schemas/MessageObject' + required: + - event + - data + description: Occurs when a [message](/docs/api-reference/messages/object) is completed. + x-oaiMeta: + dataDescription: '`data` is a [message](/docs/api-reference/messages/object)' + - type: object + properties: + event: + type: string + enum: + - thread.message.incomplete + data: + $ref: '#/components/schemas/MessageObject' + required: + - event + - data + description: Occurs when a [message](/docs/api-reference/messages/object) ends before it is completed. + x-oaiMeta: + dataDescription: '`data` is a [message](/docs/api-reference/messages/object)' + ErrorEvent: + type: object + properties: + event: + type: string + enum: + - error + data: + $ref: '#/components/schemas/Error' + required: + - event + - data + description: Occurs when an [error](/docs/guides/error-codes/api-errors) occurs. This can happen due to an internal server error or a timeout. + x-oaiMeta: + dataDescription: '`data` is an [error](/docs/guides/error-codes/api-errors)' + DoneEvent: + type: object + properties: + event: + type: string + enum: + - done + data: + type: string + enum: + - '[DONE]' + required: + - event + - data + description: Occurs when a stream ends. + x-oaiMeta: + dataDescription: '`data` is `[DONE]`' + Batch: + type: object + properties: + id: + type: string + object: + type: string + enum: + - batch + description: The object type, which is always `batch`. + endpoint: + type: string + description: The OpenAI API endpoint used by the batch. + errors: + type: object + properties: + object: + type: string + description: The object type, which is always `list`. + data: + type: array + items: + type: object + properties: + code: + type: string + description: An error code identifying the error type. + message: + type: string + description: A human-readable message providing more details about the error. + param: + type: string + description: The name of the parameter that caused the error, if applicable. + nullable: true + line: + type: integer + description: The line number of the input file where the error occurred, if applicable. + nullable: true + input_file_id: + type: string + description: The ID of the input file for the batch. + completion_window: + type: string + description: The time frame within which the batch should be processed. + status: + type: string + description: The current status of the batch. + enum: + - validating + - failed + - in_progress + - finalizing + - completed + - expired + - cancelling + - cancelled + output_file_id: + type: string + description: The ID of the file containing the outputs of successfully executed requests. + error_file_id: + type: string + description: The ID of the file containing the outputs of requests with errors. + created_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch was created. + in_progress_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch started processing. + expires_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch will expire. + finalizing_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch started finalizing. + completed_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch was completed. + failed_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch failed. + expired_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch expired. + cancelling_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch started cancelling. + cancelled_at: + type: integer + description: The Unix timestamp (in seconds) for when the batch was cancelled. + request_counts: + type: object + properties: + total: + type: integer + description: Total number of requests in the batch. + completed: + type: integer + description: Number of requests that have been completed successfully. + failed: + type: integer + description: Number of requests that have failed. + required: + - total + - completed + - failed + description: The request counts for different statuses within the batch. + metadata: + description: | + Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long. + type: object + x-oaiTypeLabel: map + nullable: true + required: + - id + - object + - endpoint + - input_file_id + - completion_window + - status + - created_at + x-oaiMeta: + name: The batch object + example: | + { + "id": "batch_abc123", + "object": "batch", + "endpoint": "/v1/completions", + "errors": null, + "input_file_id": "file-abc123", + "completion_window": "24h", + "status": "completed", + "output_file_id": "file-cvaTdG", + "error_file_id": "file-HOWS94", + "created_at": 1711471533, + "in_progress_at": 1711471538, + "expires_at": 1711557933, + "finalizing_at": 1711493133, + "completed_at": 1711493163, + "failed_at": null, + "expired_at": null, + "cancelling_at": null, + "cancelled_at": null, + "request_counts": { + "total": 100, + "completed": 95, + "failed": 5 + }, + "metadata": { + "customer_id": "user_123456789", + "batch_description": "Nightly eval job", + } + } + BatchRequestInput: + type: object + description: The per-line object of the batch input file + properties: + custom_id: + type: string + description: A developer-provided per-request id that will be used to match outputs to inputs. Must be unique for each request in a batch. + method: + type: string + enum: + - POST + description: The HTTP method to be used for the request. Currently only `POST` is supported. + url: + type: string + description: The OpenAI API relative URL to be used for the request. Currently `/v1/chat/completions`, `/v1/embeddings`, and `/v1/completions` are supported. + x-oaiMeta: + name: The request input object + example: | + {"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is 2+2?"}]}} + BatchRequestOutput: + type: object + description: The per-line object of the batch output and error files + properties: + id: + type: string + custom_id: + type: string + description: A developer-provided per-request id that will be used to match outputs to inputs. + response: + type: object + nullable: true + properties: + status_code: + type: integer + description: The HTTP status code of the response + request_id: + type: string + description: An unique identifier for the OpenAI API request. Please include this request ID when contacting support. + body: + type: object + x-oaiTypeLabel: map + description: The JSON body of the response + error: + type: object + nullable: true + description: For requests that failed with a non-HTTP error, this will contain more information on the cause of the failure. + properties: + code: + type: string + description: A machine-readable error code. + message: + type: string + description: A human-readable error message. + x-oaiMeta: + name: The request output object + example: | + {"id": "batch_req_wnaDys", "custom_id": "request-2", "response": {"status_code": 200, "request_id": "req_c187b3", "body": {"id": "chatcmpl-9758Iw", "object": "chat.completion", "created": 1711475054, "model": "gpt-4o-mini", "choices": [{"index": 0, "message": {"role": "assistant", "content": "2 + 2 equals 4."}, "finish_reason": "stop"}], "usage": {"prompt_tokens": 24, "completion_tokens": 15, "total_tokens": 39}, "system_fingerprint": null}}, "error": null} + ListBatchesResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Batch' + first_id: + type: string + example: batch_abc123 + last_id: + type: string + example: batch_abc456 + has_more: + type: boolean + object: + type: string + enum: + - list + required: + - object + - data + - has_more + AuditLogActorServiceAccount: + type: object + description: The service account that performed the audit logged action. + properties: + id: + type: string + description: The service account id. + AuditLogActorUser: + type: object + description: The user who performed the audit logged action. + properties: + id: + type: string + description: The user id. + email: + type: string + description: The user email. + AuditLogActorApiKey: + type: object + description: The API Key used to perform the audit logged action. + properties: + id: + type: string + description: The tracking id of the API key. + type: + type: string + description: The type of API key. Can be either `user` or `service_account`. + enum: + - user + - service_account + user: + $ref: '#/components/schemas/AuditLogActorUser' + service_account: + $ref: '#/components/schemas/AuditLogActorServiceAccount' + AuditLogActorSession: + type: object + description: The session in which the audit logged action was performed. + properties: + user: + $ref: '#/components/schemas/AuditLogActorUser' + ip_address: + type: string + description: The IP address from which the action was performed. + AuditLogActor: + type: object + description: The actor who performed the audit logged action. + properties: + type: + type: string + description: The type of actor. Is either `session` or `api_key`. + enum: + - session + - api_key + session: + type: object + $ref: '#/components/schemas/AuditLogActorSession' + api_key: + type: object + $ref: '#/components/schemas/AuditLogActorApiKey' + AuditLogEventType: + type: string + description: The event type. + x-oaiExpandable: true + enum: + - api_key.created + - api_key.updated + - api_key.deleted + - invite.sent + - invite.accepted + - invite.deleted + - login.succeeded + - login.failed + - logout.succeeded + - logout.failed + - organization.updated + - project.created + - project.updated + - project.archived + - service_account.created + - service_account.updated + - service_account.deleted + - user.added + - user.updated + - user.deleted + AuditLog: + type: object + description: A log of a user action or configuration change within this organization. + properties: + id: + type: string + description: The ID of this log. + type: + $ref: '#/components/schemas/AuditLogEventType' + effective_at: + type: integer + description: The Unix timestamp (in seconds) of the event. + project: + type: object + description: The project that the action was scoped to. Absent for actions not scoped to projects. + properties: + id: + type: string + description: The project ID. + name: + type: string + description: The project title. + actor: + $ref: '#/components/schemas/AuditLogActor' + api_key.created: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The tracking ID of the API key. + data: + type: object + description: The payload used to create the API key. + properties: + scopes: + type: array + items: + type: string + description: A list of scopes allowed for the API key, e.g. `["api.model.request"]` + api_key.updated: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The tracking ID of the API key. + changes_requested: + type: object + description: The payload used to update the API key. + properties: + scopes: + type: array + items: + type: string + description: A list of scopes allowed for the API key, e.g. `["api.model.request"]` + api_key.deleted: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The tracking ID of the API key. + invite.sent: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The ID of the invite. + data: + type: object + description: The payload used to create the invite. + properties: + email: + type: string + description: The email invited to the organization. + role: + type: string + description: The role the email was invited to be. Is either `owner` or `member`. + invite.accepted: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The ID of the invite. + invite.deleted: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The ID of the invite. + login.failed: + type: object + description: The details for events with this `type`. + properties: + error_code: + type: string + description: The error code of the failure. + error_message: + type: string + description: The error message of the failure. + logout.failed: + type: object + description: The details for events with this `type`. + properties: + error_code: + type: string + description: The error code of the failure. + error_message: + type: string + description: The error message of the failure. + organization.updated: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The organization ID. + changes_requested: + type: object + description: The payload used to update the organization settings. + properties: + title: + type: string + description: The organization title. + description: + type: string + description: The organization description. + name: + type: string + description: The organization name. + settings: + type: object + properties: + threads_ui_visibility: + type: string + description: Visibility of the threads page which shows messages created with the Assistants API and Playground. One of `ANY_ROLE`, `OWNERS`, or `NONE`. + usage_dashboard_visibility: + type: string + description: Visibility of the usage dashboard which shows activity and costs for your organization. One of `ANY_ROLE` or `OWNERS`. + project.created: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The project ID. + data: + type: object + description: The payload used to create the project. + properties: + name: + type: string + description: The project name. + title: + type: string + description: The title of the project as seen on the dashboard. + project.updated: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The project ID. + changes_requested: + type: object + description: The payload used to update the project. + properties: + title: + type: string + description: The title of the project as seen on the dashboard. + project.archived: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The project ID. + service_account.created: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The service account ID. + data: + type: object + description: The payload used to create the service account. + properties: + role: + type: string + description: The role of the service account. Is either `owner` or `member`. + service_account.updated: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The service account ID. + changes_requested: + type: object + description: The payload used to updated the service account. + properties: + role: + type: string + description: The role of the service account. Is either `owner` or `member`. + service_account.deleted: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The service account ID. + user.added: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The user ID. + data: + type: object + description: The payload used to add the user to the project. + properties: + role: + type: string + description: The role of the user. Is either `owner` or `member`. + user.updated: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The project ID. + changes_requested: + type: object + description: The payload used to update the user. + properties: + role: + type: string + description: The role of the user. Is either `owner` or `member`. + user.deleted: + type: object + description: The details for events with this `type`. + properties: + id: + type: string + description: The user ID. + required: + - id + - type + - effective_at + - actor + x-oaiMeta: + name: The audit log object + example: | + { + "id": "req_xxx_20240101", + "type": "api_key.created", + "effective_at": 1720804090, + "actor": { + "type": "session", + "session": { + "user": { + "id": "user-xxx", + "email": "user@example.com" + }, + "ip_address": "127.0.0.1", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + } + }, + "api_key.created": { + "id": "key_xxxx", + "data": { + "scopes": ["resource.operation"] + } + } + } + ListAuditLogsResponse: + type: object + properties: + object: + type: string + enum: + - list + data: + type: array + items: + $ref: '#/components/schemas/AuditLog' + first_id: + type: string + example: audit_log-defb456h8dks + last_id: + type: string + example: audit_log-hnbkd8s93s + has_more: + type: boolean + required: + - object + - data + - first_id + - last_id + - has_more + Invite: + type: object + description: Represents an individual `invite` to the organization. + properties: + object: + type: string + enum: + - organization.invite + description: The object type, which is always `organization.invite` + id: + type: string + description: The identifier, which can be referenced in API endpoints + email: + type: string + description: The email address of the individual to whom the invite was sent + role: + type: string + enum: + - owner + - reader + description: '`owner` or `reader`' + status: + type: string + enum: + - accepted + - expired + - pending + description: '`accepted`,`expired`, or `pending`' + invited_at: + type: integer + description: The Unix timestamp (in seconds) of when the invite was sent. + expires_at: + type: integer + description: The Unix timestamp (in seconds) of when the invite expires. + accepted_at: + type: integer + description: The Unix timestamp (in seconds) of when the invite was accepted. + required: + - object + - id + - email + - role + - status + - invited_at + - expires_at + x-oaiMeta: + name: The invite object + example: | + { + "object": "organization.invite", + "id": "invite-abc", + "email": "user@example.com", + "role": "owner", + "status": "accepted", + "invited_at": 1711471533, + "expires_at": 1711471533, + "accepted_at": 1711471533 + } + InviteListResponse: + type: object + properties: + object: + type: string + enum: + - list + description: The object type, which is always `list` + data: + type: array + items: + $ref: '#/components/schemas/Invite' + first_id: + type: string + description: The first `invite_id` in the retrieved `list` + last_id: + type: string + description: The last `invite_id` in the retrieved `list` + has_more: + type: boolean + description: The `has_more` property is used for pagination to indicate there are additional results. + required: + - object + - data + InviteRequest: + type: object + properties: + email: + type: string + description: Send an email to this address + role: + type: string + enum: + - reader + - owner + description: '`owner` or `reader`' + required: + - email + - role + InviteDeleteResponse: + type: object + properties: + object: + type: string + enum: + - organization.invite.deleted + description: The object type, which is always `organization.invite.deleted` + id: + type: string + deleted: + type: boolean + required: + - object + - id + - deleted + User: + type: object + description: Represents an individual `user` within an organization. + properties: + object: + type: string + enum: + - organization.user + description: The object type, which is always `organization.user` + id: + type: string + description: The identifier, which can be referenced in API endpoints + name: + type: string + description: The name of the user + email: + type: string + description: The email address of the user + role: + type: string + enum: + - owner + - reader + description: '`owner` or `reader`' + added_at: + type: integer + description: The Unix timestamp (in seconds) of when the user was added. + required: + - object + - id + - name + - email + - role + - added_at + x-oaiMeta: + name: The user object + example: | + { + "object": "organization.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + UserListResponse: + type: object + properties: + object: + type: string + enum: + - list + data: + type: array + items: + $ref: '#/components/schemas/User' + first_id: + type: string + last_id: + type: string + has_more: + type: boolean + required: + - object + - data + - first_id + - last_id + - has_more + UserRoleUpdateRequest: + type: object + properties: + role: + type: string + enum: + - owner + - reader + description: '`owner` or `reader`' + required: + - role + UserDeleteResponse: + type: object + properties: + object: + type: string + enum: + - organization.user.deleted + id: + type: string + deleted: + type: boolean + required: + - object + - id + - deleted + Project: + type: object + description: Represents an individual project. + properties: + id: + type: string + description: The identifier, which can be referenced in API endpoints + object: + type: string + enum: + - organization.project + description: The object type, which is always `organization.project` + name: + type: string + description: The name of the project. This appears in reporting. + created_at: + type: integer + description: The Unix timestamp (in seconds) of when the project was created. + archived_at: + type: integer + nullable: true + description: The Unix timestamp (in seconds) of when the project was archived or `null`. + status: + type: string + enum: + - active + - archived + description: '`active` or `archived`' + required: + - id + - object + - name + - created_at + - status + x-oaiMeta: + name: The project object + example: | + { + "id": "proj_abc", + "object": "organization.project", + "name": "Project example", + "created_at": 1711471533, + "archived_at": null, + "status": "active" + } + ProjectListResponse: + type: object + properties: + object: + type: string + enum: + - list + data: + type: array + items: + $ref: '#/components/schemas/Project' + first_id: + type: string + last_id: + type: string + has_more: + type: boolean + required: + - object + - data + - first_id + - last_id + - has_more + ProjectCreateRequest: + type: object + properties: + name: + type: string + description: The friendly name of the project, this name appears in reports. + required: + - name + ProjectUpdateRequest: + type: object + properties: + name: + type: string + description: The updated name of the project, this name appears in reports. + required: + - name + DefaultProjectErrorResponse: + type: object + properties: + code: + type: integer + message: + type: string + required: + - code + - message + ProjectUser: + type: object + description: Represents an individual user in a project. + properties: + object: + type: string + enum: + - organization.project.user + description: The object type, which is always `organization.project.user` + id: + type: string + description: The identifier, which can be referenced in API endpoints + name: + type: string + description: The name of the user + email: + type: string + description: The email address of the user + role: + type: string + enum: + - owner + - member + description: '`owner` or `member`' + added_at: + type: integer + description: The Unix timestamp (in seconds) of when the project was added. + required: + - object + - id + - name + - email + - role + - added_at + x-oaiMeta: + name: The project user object + example: | + { + "object": "organization.project.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + ProjectUserListResponse: + type: object + properties: + object: + type: string + data: + type: array + items: + $ref: '#/components/schemas/ProjectUser' + first_id: + type: string + last_id: + type: string + has_more: + type: boolean + required: + - object + - data + - first_id + - last_id + - has_more + ProjectUserCreateRequest: + type: object + properties: + user_id: + type: string + description: The ID of the user. + role: + type: string + enum: + - owner + - member + description: '`owner` or `member`' + required: + - user_id + - role + ProjectUserUpdateRequest: + type: object + properties: + role: + type: string + enum: + - owner + - member + description: '`owner` or `member`' + required: + - role + ProjectUserDeleteResponse: + type: object + properties: + object: + type: string + enum: + - organization.project.user.deleted + id: + type: string + deleted: + type: boolean + required: + - object + - id + - deleted + ProjectServiceAccount: + type: object + description: Represents an individual service account in a project. + properties: + object: + type: string + enum: + - organization.project.service_account + description: The object type, which is always `organization.project.service_account` + id: + type: string + description: The identifier, which can be referenced in API endpoints + name: + type: string + description: The name of the service account + role: + type: string + enum: + - owner + - member + description: '`owner` or `member`' + created_at: + type: integer + description: The Unix timestamp (in seconds) of when the service account was created + required: + - object + - id + - name + - role + - created_at + x-oaiMeta: + name: The project service account object + example: | + { + "object": "organization.project.service_account", + "id": "svc_acct_abc", + "name": "Service Account", + "role": "owner", + "created_at": 1711471533 + } + ProjectServiceAccountListResponse: + type: object + properties: + object: + type: string + enum: + - list + data: + type: array + items: + $ref: '#/components/schemas/ProjectServiceAccount' + first_id: + type: string + last_id: + type: string + has_more: + type: boolean + required: + - object + - data + - first_id + - last_id + - has_more + ProjectServiceAccountCreateRequest: + type: object + properties: + name: + type: string + description: The name of the service account being created. + required: + - name + ProjectServiceAccountCreateResponse: + type: object + properties: + object: + type: string + enum: + - organization.project.service_account + id: + type: string + name: + type: string + role: + type: string + enum: + - member + description: Service accounts can only have one role of type `member` + created_at: + type: integer + api_key: + $ref: '#/components/schemas/ProjectServiceAccountApiKey' + required: + - object + - id + - name + - role + - created_at + - api_key + ProjectServiceAccountApiKey: + type: object + properties: + object: + type: string + enum: + - organization.project.service_account.api_key + description: The object type, which is always `organization.project.service_account.api_key` + value: + type: string + name: + type: string + created_at: + type: integer + id: + type: string + required: + - object + - value + - name + - created_at + - id + ProjectServiceAccountDeleteResponse: + type: object + properties: + object: + type: string + enum: + - organization.project.service_account.deleted + id: + type: string + deleted: + type: boolean + required: + - object + - id + - deleted + ProjectApiKey: + type: object + description: Represents an individual API key in a project. + properties: + object: + type: string + enum: + - organization.project.api_key + description: The object type, which is always `organization.project.api_key` + redacted_value: + type: string + description: The redacted value of the API key + name: + type: string + description: The name of the API key + created_at: + type: integer + description: The Unix timestamp (in seconds) of when the API key was created + id: + type: string + description: The identifier, which can be referenced in API endpoints + owner: + type: object + properties: + type: + type: string + enum: + - user + - service_account + description: '`user` or `service_account`' + user: + $ref: '#/components/schemas/ProjectUser' + service_account: + $ref: '#/components/schemas/ProjectServiceAccount' + required: + - object + - redacted_value + - name + - created_at + - id + - owner + x-oaiMeta: + name: The project API key object + example: | + { + "object": "organization.project.api_key", + "redacted_value": "sk-abc...def", + "name": "My API Key", + "created_at": 1711471533, + "id": "key_abc", + "owner": { + "type": "user", + "user": { + "object": "organization.project.user", + "id": "user_abc", + "name": "First Last", + "email": "user@example.com", + "role": "owner", + "added_at": 1711471533 + } + } + } + ProjectApiKeyListResponse: + type: object + properties: + object: + type: string + enum: + - list + data: + type: array + items: + $ref: '#/components/schemas/ProjectApiKey' + first_id: + type: string + last_id: + type: string + has_more: + type: boolean + required: + - object + - data + - first_id + - last_id + - has_more + ProjectApiKeyDeleteResponse: + type: object + properties: + object: + type: string + enum: + - organization.project.api_key.deleted + id: + type: string + deleted: + type: boolean + required: + - object + - id + - deleted +security: + - ApiKeyAuth: [] +x-oaiMeta: + navigationGroups: + - id: endpoints + title: Endpoints + - id: assistants + title: Assistants + - id: administration + title: Administration + - id: legacy + title: Legacy + groups: + - id: audio + title: Audio + description: | + Learn how to turn audio into text or text into audio. + + Related guide: [Speech to text](/docs/guides/speech-to-text) + navigationGroup: endpoints + sections: + - type: endpoint + key: createSpeech + path: createSpeech + - type: endpoint + key: createTranscription + path: createTranscription + - type: endpoint + key: createTranslation + path: createTranslation + - type: object + key: CreateTranscriptionResponseJson + path: json-object + - type: object + key: CreateTranscriptionResponseVerboseJson + path: verbose-json-object + - id: chat + title: Chat + description: | + Given a list of messages comprising a conversation, the model will return a response. + + Related guide: [Chat Completions](/docs/guides/text-generation) + navigationGroup: endpoints + sections: + - type: endpoint + key: createChatCompletion + path: create + - type: object + key: CreateChatCompletionResponse + path: object + - type: object + key: CreateChatCompletionStreamResponse + path: streaming + - id: embeddings + title: Embeddings + description: | + Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. + + Related guide: [Embeddings](/docs/guides/embeddings) + navigationGroup: endpoints + sections: + - type: endpoint + key: createEmbedding + path: create + - type: object + key: Embedding + path: object + - id: fine-tuning + title: Fine-tuning + description: | + Manage fine-tuning jobs to tailor a model to your specific training data. + + Related guide: [Fine-tune models](/docs/guides/fine-tuning) + navigationGroup: endpoints + sections: + - type: endpoint + key: createFineTuningJob + path: create + - type: endpoint + key: listPaginatedFineTuningJobs + path: list + - type: endpoint + key: listFineTuningEvents + path: list-events + - type: endpoint + key: listFineTuningJobCheckpoints + path: list-checkpoints + - type: endpoint + key: retrieveFineTuningJob + path: retrieve + - type: endpoint + key: cancelFineTuningJob + path: cancel + - type: object + key: FinetuneChatRequestInput + path: chat-input + - type: object + key: FinetuneCompletionRequestInput + path: completions-input + - type: object + key: FineTuningJob + path: object + - type: object + key: FineTuningJobEvent + path: event-object + - type: object + key: FineTuningJobCheckpoint + path: checkpoint-object + - id: batch + title: Batch + description: | + Create large batches of API requests for asynchronous processing. The Batch API returns completions within 24 hours for a 50% discount. + + Related guide: [Batch](/docs/guides/batch) + navigationGroup: endpoints + sections: + - type: endpoint + key: createBatch + path: create + - type: endpoint + key: retrieveBatch + path: retrieve + - type: endpoint + key: cancelBatch + path: cancel + - type: endpoint + key: listBatches + path: list + - type: object + key: Batch + path: object + - type: object + key: BatchRequestInput + path: request-input + - type: object + key: BatchRequestOutput + path: request-output + - id: files + title: Files + description: | + Files are used to upload documents that can be used with features like [Assistants](/docs/api-reference/assistants), [Fine-tuning](/docs/api-reference/fine-tuning), and [Batch API](/docs/guides/batch). + navigationGroup: endpoints + sections: + - type: endpoint + key: createFile + path: create + - type: endpoint + key: listFiles + path: list + - type: endpoint + key: retrieveFile + path: retrieve + - type: endpoint + key: deleteFile + path: delete + - type: endpoint + key: downloadFile + path: retrieve-contents + - type: object + key: OpenAIFile + path: object + - id: uploads + title: Uploads + description: | + Allows you to upload large files in multiple parts. + navigationGroup: endpoints + sections: + - type: endpoint + key: createUpload + path: create + - type: endpoint + key: addUploadPart + path: add-part + - type: endpoint + key: completeUpload + path: complete + - type: endpoint + key: cancelUpload + path: cancel + - type: object + key: Upload + path: object + - type: object + key: UploadPart + path: part-object + - id: images + title: Images + description: | + Given a prompt and/or an input image, the model will generate a new image. + + Related guide: [Image generation](/docs/guides/images) + navigationGroup: endpoints + sections: + - type: endpoint + key: createImage + path: create + - type: endpoint + key: createImageEdit + path: createEdit + - type: endpoint + key: createImageVariation + path: createVariation + - type: object + key: Image + path: object + - id: models + title: Models + description: | + List and describe the various models available in the API. You can refer to the [Models](/docs/models) documentation to understand what models are available and the differences between them. + navigationGroup: endpoints + sections: + - type: endpoint + key: listModels + path: list + - type: endpoint + key: retrieveModel + path: retrieve + - type: endpoint + key: deleteModel + path: delete + - type: object + key: Model + path: object + - id: moderations + title: Moderations + description: | + Given some input text, outputs if the model classifies it as potentially harmful across several categories. + + Related guide: [Moderations](/docs/guides/moderation) + navigationGroup: endpoints + sections: + - type: endpoint + key: createModeration + path: create + - type: object + key: CreateModerationResponse + path: object + - id: assistants + title: Assistants + beta: true + description: | + Build assistants that can call models and use tools to perform tasks. + + [Get started with the Assistants API](/docs/assistants) + navigationGroup: assistants + sections: + - type: endpoint + key: createAssistant + path: createAssistant + - type: endpoint + key: listAssistants + path: listAssistants + - type: endpoint + key: getAssistant + path: getAssistant + - type: endpoint + key: modifyAssistant + path: modifyAssistant + - type: endpoint + key: deleteAssistant + path: deleteAssistant + - type: object + key: AssistantObject + path: object + - id: threads + title: Threads + beta: true + description: | + Create threads that assistants can interact with. + + Related guide: [Assistants](/docs/assistants/overview) + navigationGroup: assistants + sections: + - type: endpoint + key: createThread + path: createThread + - type: endpoint + key: getThread + path: getThread + - type: endpoint + key: modifyThread + path: modifyThread + - type: endpoint + key: deleteThread + path: deleteThread + - type: object + key: ThreadObject + path: object + - id: messages + title: Messages + beta: true + description: | + Create messages within threads + + Related guide: [Assistants](/docs/assistants/overview) + navigationGroup: assistants + sections: + - type: endpoint + key: createMessage + path: createMessage + - type: endpoint + key: listMessages + path: listMessages + - type: endpoint + key: getMessage + path: getMessage + - type: endpoint + key: modifyMessage + path: modifyMessage + - type: endpoint + key: deleteMessage + path: deleteMessage + - type: object + key: MessageObject + path: object + - id: runs + title: Runs + beta: true + description: | + Represents an execution run on a thread. + + Related guide: [Assistants](/docs/assistants/overview) + navigationGroup: assistants + sections: + - type: endpoint + key: createRun + path: createRun + - type: endpoint + key: createThreadAndRun + path: createThreadAndRun + - type: endpoint + key: listRuns + path: listRuns + - type: endpoint + key: getRun + path: getRun + - type: endpoint + key: modifyRun + path: modifyRun + - type: endpoint + key: submitToolOuputsToRun + path: submitToolOutputs + - type: endpoint + key: cancelRun + path: cancelRun + - type: object + key: RunObject + path: object + - id: run-steps + title: Run Steps + beta: true + description: | + Represents the steps (model and tool calls) taken during the run. + + Related guide: [Assistants](/docs/assistants/overview) + navigationGroup: assistants + sections: + - type: endpoint + key: listRunSteps + path: listRunSteps + - type: endpoint + key: getRunStep + path: getRunStep + - type: object + key: RunStepObject + path: step-object + - id: vector-stores + title: Vector Stores + beta: true + description: | + Vector stores are used to store files for use by the `file_search` tool. + + Related guide: [File Search](/docs/assistants/tools/file-search) + navigationGroup: assistants + sections: + - type: endpoint + key: createVectorStore + path: create + - type: endpoint + key: listVectorStores + path: list + - type: endpoint + key: getVectorStore + path: retrieve + - type: endpoint + key: modifyVectorStore + path: modify + - type: endpoint + key: deleteVectorStore + path: delete + - type: object + key: VectorStoreObject + path: object + - id: vector-stores-files + title: Vector Store Files + beta: true + description: | + Vector store files represent files inside a vector store. + + Related guide: [File Search](/docs/assistants/tools/file-search) + navigationGroup: assistants + sections: + - type: endpoint + key: createVectorStoreFile + path: createFile + - type: endpoint + key: listVectorStoreFiles + path: listFiles + - type: endpoint + key: getVectorStoreFile + path: getFile + - type: endpoint + key: deleteVectorStoreFile + path: deleteFile + - type: object + key: VectorStoreFileObject + path: file-object + - id: vector-stores-file-batches + title: Vector Store File Batches + beta: true + description: | + Vector store file batches represent operations to add multiple files to a vector store. + + Related guide: [File Search](/docs/assistants/tools/file-search) + navigationGroup: assistants + sections: + - type: endpoint + key: createVectorStoreFileBatch + path: createBatch + - type: endpoint + key: getVectorStoreFileBatch + path: getBatch + - type: endpoint + key: cancelVectorStoreFileBatch + path: cancelBatch + - type: endpoint + key: listFilesInVectorStoreBatch + path: listBatchFiles + - type: object + key: VectorStoreFileBatchObject + path: batch-object + - id: assistants-streaming + title: Streaming + beta: true + description: | + Stream the result of executing a Run or resuming a Run after submitting tool outputs. + + You can stream events from the [Create Thread and Run](/docs/api-reference/runs/createThreadAndRun), + [Create Run](/docs/api-reference/runs/createRun), and [Submit Tool Outputs](/docs/api-reference/runs/submitToolOutputs) + endpoints by passing `"stream": true`. The response will be a [Server-Sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) stream. + + Our Node and Python SDKs provide helpful utilities to make streaming easy. Reference the + [Assistants API quickstart](/docs/assistants/overview) to learn more. + navigationGroup: assistants + sections: + - type: object + key: MessageDeltaObject + path: message-delta-object + - type: object + key: RunStepDeltaObject + path: run-step-delta-object + - type: object + key: AssistantStreamEvent + path: events + - id: administration + title: Overview + description: | + Programmatically manage your organization. + + The Audit Logs endpoint provides a log of all actions taken in the + organization for security and monitoring purposes. + + To access these endpoints please generate an Admin API Key through the [API Platform Organization overview](/organization/admin-keys). Admin API keys cannot be used for non-administration endpoints. + + For best practices on setting up your organization, please refer to this [guide](/docs/guides/production-best-practices/setting-up-your-organization) + navigationGroup: administration + - id: invite + title: Invites + description: Invite and manage invitations for an organization. Invited users are automatically added to the Default project. + navigationGroup: administration + sections: + - type: endpoint + key: list-invites + path: list + - type: endpoint + key: inviteUser + path: create + - type: endpoint + key: retrieve-invite + path: retrieve + - type: endpoint + key: delete-invite + path: delete + - type: object + key: Invite + path: object + - id: users + title: Users + description: | + Manage users and their role in an organization. Users will be automatically added to the Default project. + navigationGroup: administration + sections: + - type: endpoint + key: list-users + path: list + - type: endpoint + key: modify-user + path: modify + - type: endpoint + key: retrieve-user + path: retrieve + - type: endpoint + key: delete-user + path: delete + - type: object + key: User + path: object + - id: projects + title: Projects + description: | + Manage the projects within an orgnanization includes creation, updating, and archiving or projects. + The Default project cannot be modified or archived. + navigationGroup: administration + sections: + - type: endpoint + key: list-projects + path: list + - type: endpoint + key: create-project + path: create + - type: endpoint + key: retrieve-project + path: retrieve + - type: endpoint + key: modify-project + path: modify + - type: endpoint + key: archive-project + path: archive + - type: object + key: Project + path: object + - id: project-users + title: Project Users + description: | + Manage users within a project, including adding, updating roles, and removing users. + Users cannot be removed from the Default project, unless they are being removed from the organization. + navigationGroup: administration + sections: + - type: endpoint + key: list-project-users + path: list + - type: endpoint + key: create-project-user + path: creeate + - type: endpoint + key: retrieve-project-user + path: retrieve + - type: endpoint + key: modify-project-user + path: modify + - type: endpoint + key: delete-project-user + path: delete + - type: object + key: ProjectUser + path: object + - id: project-service-accounts + title: Project Service Accounts + description: | + Manage service accounts within a project. A service account is a bot user that is not associated with a user. + If a user leaves an organization, their keys and membership in projects will no longer work. Service accounts + do not have this limitation. However, service accounts can also be deleted from a project. + navigationGroup: administration + sections: + - type: endpoint + key: list-project-service-accounts + path: list + - type: endpoint + key: create-project-service-account + path: create + - type: endpoint + key: retrieve-project-service-account + path: retrieve + - type: endpoint + key: delete-project-service-account + path: delete + - type: object + key: ProjectServiceAccount + path: object + - id: project-api-keys + title: Project API Keys + description: | + Manage API keys for a given project. Supports listing and deleting keys for users. + This API does not allow issuing keys for users, as users need to authorize themselves to generate keys. + navigationGroup: administration + sections: + - type: endpoint + key: list-project-api-keys + path: list + - type: endpoint + key: retrieve-project-api-key + path: retrieve + - type: endpoint + key: delete-project-api-key + path: delete + - type: object + key: ProjectApiKey + path: object + - id: audit-logs + title: Audit Logs + description: | + Logs of user actions and configuration changes within this organization. + + To log events, you must activate logging in the [Organization Settings](/settings/organization/general). + Once activated, for security reasons, logging cannot be deactivated. + navigationGroup: administration + sections: + - type: endpoint + key: list-audit-logs + path: list + - type: object + key: AuditLog + path: object + - id: completions + title: Completions + legacy: true + navigationGroup: legacy + description: | + Given a prompt, the model will return one or more predicted completions along with the probabilities of alternative tokens at each position. Most developer should use our [Chat Completions API](/docs/guides/text-generation/text-generation-models) to leverage our best and newest models. + sections: + - type: endpoint + key: createCompletion + path: create + - type: object + key: CreateCompletionResponse + path: object \ No newline at end of file From f9d978b14b8a1f91f6ec61d48da01e0c90dcccb9 Mon Sep 17 00:00:00 2001 From: pamaljayasinghe Date: Wed, 27 May 2026 09:17:25 +0530 Subject: [PATCH 03/12] Multi Provider Routing Policy changes --- .../openai-header-router.yaml | 78 ++++++ .../default-policies/openai-to-anthropic.yaml | 84 ++++++ .../openai-to-azure-openai.yaml | 78 ++++++ .../default-policies/openai-to-mistral.yaml | 61 +++++ gateway/build-manifest.yaml | 15 ++ gateway/build.yaml | 10 + gateway/examples/anthropic-openai-proxy.yaml | 42 +++ gateway/examples/anthropic-provider.yaml | 62 +++++ gateway/examples/azure-openai-provider.yaml | 67 +++++ gateway/examples/azure-openai-proxy.yaml | 37 +++ gateway/examples/gemini-provider.yaml | 58 +++++ gateway/examples/llm-provider.yaml | 38 +++ gateway/examples/llm-proxy.yaml | 4 + gateway/examples/mistral-openai-proxy.yaml | 43 +++ gateway/examples/mistral-provider.yaml | 63 +++++ .../examples/openai-multi-provider-proxy.yaml | 151 +++++++++++ gateway/examples/openai-provider.yaml | 67 +++++ .../api/management-openapi.yaml | 35 +++ .../pkg/api/management/generated.go | 13 + .../pkg/config/llm_validator.go | 75 ++++++ .../pkg/utils/llm_transformer.go | 134 +++++++--- .../pkg/utils/llm_transformer_test.go | 128 +++++++++ .../slugify-body/policy-definition.yaml | 19 +- .../analytics/policy-definition.yaml | 7 +- .../api/v1alpha1/llmproxy_types.go | 22 ++ .../api/v1alpha1/zz_generated.deepcopy.go | 32 +++ ...eway.api-platform.wso2.com_llmproxies.yaml | 79 ++++++ .../controller/llmproxy_controller.go | 38 ++- .../controller/llmproxy_controller_test.go | 82 ++++++ .../management_upstream_auth_payload.go | 20 ++ .../management_valuefrom_enqueue.go | 9 +- .../management_valuefrom_fingerprint.go | 6 +- ...eway.api-platform.wso2.com_llmproxies.yaml | 79 ++++++ mistral-proxy.yaml | 246 ++++++++++++++++++ platform-api/src/api/generated.go | 12 + .../src/internal/dto/llm_deployment.go | 18 +- platform-api/src/internal/model/llm.go | 26 +- platform-api/src/internal/service/llm.go | 59 ++++- .../src/internal/service/llm_deployment.go | 17 +- platform-api/src/resources/openapi.yaml | 31 +++ 40 files changed, 2057 insertions(+), 88 deletions(-) create mode 100644 event-gateway/default-policies/openai-header-router.yaml create mode 100644 event-gateway/default-policies/openai-to-anthropic.yaml create mode 100644 event-gateway/default-policies/openai-to-azure-openai.yaml create mode 100644 event-gateway/default-policies/openai-to-mistral.yaml create mode 100644 gateway/examples/anthropic-openai-proxy.yaml create mode 100644 gateway/examples/anthropic-provider.yaml create mode 100644 gateway/examples/azure-openai-provider.yaml create mode 100644 gateway/examples/azure-openai-proxy.yaml create mode 100644 gateway/examples/gemini-provider.yaml create mode 100644 gateway/examples/mistral-openai-proxy.yaml create mode 100644 gateway/examples/mistral-provider.yaml create mode 100644 gateway/examples/openai-multi-provider-proxy.yaml create mode 100644 gateway/examples/openai-provider.yaml create mode 100644 mistral-proxy.yaml diff --git a/event-gateway/default-policies/openai-header-router.yaml b/event-gateway/default-policies/openai-header-router.yaml new file mode 100644 index 0000000000..c7633e3e3e --- /dev/null +++ b/event-gateway/default-policies/openai-header-router.yaml @@ -0,0 +1,78 @@ +name: openai-header-router +version: v1.0.0 +description: | + Selects a target LLM provider for each incoming OpenAI Chat Completions + request by inspecting a request header, then publishes the chosen + provider id into SharedContext.Metadata["selected_provider"]. + + This policy is a "setter" — its only job is to publish the selection. + Downstream consumer policies (the per-provider translators such as + openai-to-anthropic, openai-to-azure-openai, and openai-to-mistral) + read that key and own both the translation and the upstream-cluster + selection. + + Selection algorithm: + + 1. The configured header is read from the request (case-insensitive). + 2. Each entry in "mappings" is checked: if its "headerValue" matches the + incoming header value (case-insensitive), the entry's "provider" is + selected. + 3. If the header is absent, empty, or no mapping matches, the configured + "defaultProvider" is selected. + + The defaultProvider field is required so this policy always produces a + selection — there is no "pass through with no provider" mode. + +parameters: + type: object + additionalProperties: false + properties: + headerName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the request header to read for provider selection. + Comparison is case-insensitive. + default: x-provider + minLength: 1 + defaultProvider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id selected when the header is missing, empty, or does + not match any entry in "mappings". This must be a provider id that + a downstream translator is bound to. + minLength: 1 + mappings: + type: array + x-wso2-policy-advanced-param: false + description: | + Mapping from incoming header values to provider ids. The first + matching entry wins. + items: + type: object + additionalProperties: false + properties: + headerValue: + type: string + description: | + Header value to match (case-insensitive, leading/trailing + whitespace trimmed). + minLength: 1 + provider: + type: string + description: | + Provider id published to SharedContext.Metadata + ["selected_provider"] when this entry matches. + minLength: 1 + required: + - headerValue + - provider + minItems: 1 + required: + - mappings + - defaultProvider + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-anthropic.yaml b/event-gateway/default-policies/openai-to-anthropic.yaml new file mode 100644 index 0000000000..bd695e8ce0 --- /dev/null +++ b/event-gateway/default-policies/openai-to-anthropic.yaml @@ -0,0 +1,84 @@ +name: openai-to-anthropic +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the Anthropic + Messages API format and forwards it to an Anthropic-compatible upstream. + + Request transformations: + - system/developer messages are lifted into the top-level "system" field. + - messages[] is converted to Anthropic role/content blocks. + - assistant tool_calls become tool_use content blocks; tool-role messages + are grouped into a single user message of tool_result blocks. + - tools[] / tool_choice are converted to the Anthropic equivalents. + - stop is mapped to stop_sequences; image_url parts to image blocks. + - The request path is rewritten to /v1/messages and the anthropic-version + header is set. + + Response transformations: + - The Anthropic Messages response body is rewritten into the OpenAI + ChatCompletion shape (id, object, created, model, choices, usage). + - text content blocks become choice.message.content; tool_use blocks + become choice.message.tool_calls; stop_reason is mapped to + finish_reason; cache token counts are carried in usage + .prompt_tokens_details. + - Anthropic error envelopes are rewritten into OpenAI error envelopes + so clients see consistent error shapes regardless of provider. + - Streaming (Server-Sent Events) responses are passed through + untouched in this policy version; clients that need true streaming + should request non-streaming (omit "stream": true) until a streaming + variant ships. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests pass + through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Anthropic API key must be configured on the + upstream LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Anthropic model name used in the translated request (for example + claude-sonnet-4-20250514). Overrides the OpenAI "model" field. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + anthropicVersion: + type: string + x-wso2-policy-advanced-param: true + description: | + Value of the anthropic-version request header sent upstream. + default: "2023-06-01" + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-azure-openai.yaml b/event-gateway/default-policies/openai-to-azure-openai.yaml new file mode 100644 index 0000000000..67bb75e537 --- /dev/null +++ b/event-gateway/default-policies/openai-to-azure-openai.yaml @@ -0,0 +1,78 @@ +name: openai-to-azure-openai +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the Azure + OpenAI request shape and forwards it to an Azure-OpenAI-compatible + upstream. + + Transformation: + - The request path is rewritten to + /openai/deployments/{deployment}/{pathSuffix}?api-version=. + - The body passes through unchanged — Azure OpenAI accepts the OpenAI + request shape directly. + - The deployment id is taken from the "model" parameter when set, + otherwise from the request body's "model" field. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests + pass through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Azure api-key must be configured on the upstream + LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + apiVersion: + type: string + x-wso2-policy-advanced-param: false + description: | + Azure OpenAI api-version query-string value (for example + 2024-02-15-preview). Required because Azure rejects requests + without it. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Azure deployment id used in the rewritten path. When omitted, the + request body's "model" field is used as the deployment id. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + pathSuffix: + type: string + x-wso2-policy-advanced-param: true + description: | + Endpoint suffix appended after the deployment segment in the + rewritten path. Defaults to /chat/completions. + default: /chat/completions + minLength: 1 + required: + - apiVersion + +systemParameters: + type: object + properties: {} diff --git a/event-gateway/default-policies/openai-to-mistral.yaml b/event-gateway/default-policies/openai-to-mistral.yaml new file mode 100644 index 0000000000..f2ac1d85bd --- /dev/null +++ b/event-gateway/default-policies/openai-to-mistral.yaml @@ -0,0 +1,61 @@ +name: openai-to-mistral +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the format + expected by Mistral AI's chat completions endpoint and forwards it to a + Mistral-compatible upstream. + + Mistral's chat completions API is largely OpenAI-compatible, so the + translation is small in scope: + - The "model" field is overridden with the configured Mistral model. + - OpenAI request fields that Mistral does not accept (logprobs, n, + service_tier, store, metadata, user, etc.) are stripped. + - The request path is rewritten to /v1/chat/completions. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests + pass through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Mistral API key must be configured on the upstream + LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Mistral model name used in the translated request (for example + mistral-large-latest). Overrides the OpenAI "model" field. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} diff --git a/gateway/build-manifest.yaml b/gateway/build-manifest.yaml index 3787d4e44d..cb4dec14cc 100644 --- a/gateway/build-manifest.yaml +++ b/gateway/build-manifest.yaml @@ -126,3 +126,18 @@ policies: - name: word-count-guardrail version: v1.0.2 gomodule: github.com/wso2/gateway-controllers/policies/word-count-guardrail@v1 + - name: openai-to-azure-openai + version: v1.0.0 + filePath: ./dev-policies/openai-to-azure-openai + - name: openai-to-anthropic + version: v1.0.0 + filePath: ./dev-policies/openai-to-anthropic + - name: openai-to-mistral + version: v1.0.0 + filePath: ./dev-policies/openai-to-mistral + - name: openai-to-gemini + version: v1.0.0 + filePath: ./dev-policies/openai-to-gemini + - name: openai-header-router + version: v1.0.0 + filePath: ./dev-policies/openai-header-router diff --git a/gateway/build.yaml b/gateway/build.yaml index 7e4d2cd995..205afba44e 100644 --- a/gateway/build.yaml +++ b/gateway/build.yaml @@ -86,3 +86,13 @@ policies: gomodule: github.com/wso2/gateway-controllers/policies/url-guardrail@v1 - name: word-count-guardrail gomodule: github.com/wso2/gateway-controllers/policies/word-count-guardrail@v1 + - name: openai-to-azure-openai + filePath: ./dev-policies/openai-to-azure-openai + - name: openai-to-anthropic + filePath: ./dev-policies/openai-to-anthropic + - name: openai-to-mistral + filePath: ./dev-policies/openai-to-mistral + - name: openai-to-gemini + filePath: ./dev-policies/openai-to-gemini + - name: openai-header-router + filePath: ./dev-policies/openai-header-router diff --git a/gateway/examples/anthropic-openai-proxy.yaml b/gateway/examples/anthropic-openai-proxy.yaml new file mode 100644 index 0000000000..4ac65baec2 --- /dev/null +++ b/gateway/examples/anthropic-openai-proxy.yaml @@ -0,0 +1,42 @@ +# -------------------------------------------------------------------- +# OpenAI -> Anthropic LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-anthropic` policy translates into Anthropic's Messages API +# format and forwards to the existing `anthropic-provider` upstream +# (api.anthropic.com). +# +# Single-provider mode: there is no router in front of the policy, so +# nothing sets metadata["selected_provider"] and the translator runs on +# every request. "id" is omitted, so it routes to the default upstream +# (the primary provider above). +# +# The provider must already be deployed and must have its x-api-key +# set on upstream.auth (the policy does NOT inject the Anthropic key). +# +# The provider example's api-key-auth policy expects the proxy to send the +# issued provider loopback key in X-API-Key. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-anthropic +spec: + displayName: OpenAI to Anthropic Proxy + version: v1.0 + context: /openai-anthropic + provider: + id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + policies: + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 diff --git a/gateway/examples/anthropic-provider.yaml b/gateway/examples/anthropic-provider.yaml new file mode 100644 index 0000000000..6e1e8ec75b --- /dev/null +++ b/gateway/examples/anthropic-provider.yaml @@ -0,0 +1,62 @@ +# -------------------------------------------------------------------- +# Anthropic LLM Provider Configuration +# +# Uses the built-in `anthropic` template and points to the public +# Anthropic API endpoint. Replace the placeholder with a real Anthropic +# API key (sk-ant-...) before deploying. +# +# The openai-to-anthropic policy on the proxy layer translates +# OpenAI-format requests into Anthropic's Messages API wire format +# (path /v1/messages, anthropic-version header, body rewrite). +# +# Auth: upstream.auth is the gateway -> Anthropic vendor auth. Proxies in +# front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: anthropic-provider +spec: + displayName: Anthropic Provider + version: v1.0 + template: anthropic + upstream: + url: https://api.anthropic.com + auth: + type: api-key + header: x-api-key + # Anthropic's header is x-api-key with the raw key (NO "Bearer" prefix). + value: sk-ant-REPLACE_WITH_ANTHROPIC_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/messages + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/messages + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: anthropic-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: anthropic-provider + displayName: Anthropic provider loopback key + apiKey: + value: anthropic_provider_loopback_key_1234567890abcdef + # anthropic_provider_loopback_key_1234567890abcdef + maskedApiKey: anthropic_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/azure-openai-provider.yaml b/gateway/examples/azure-openai-provider.yaml new file mode 100644 index 0000000000..e1345ac3c4 --- /dev/null +++ b/gateway/examples/azure-openai-provider.yaml @@ -0,0 +1,67 @@ +# -------------------------------------------------------------------- +# Azure OpenAI LLM Provider Configuration +# +# Uses the built-in 'azure-openai' template and points to your Azure +# OpenAI endpoint. The openai-to-azure-openai policy on the proxy layer +# translates OpenAI-format requests into the Azure OpenAI wire format +# (path rewriting, api-version query param). +# +# Replace before deploying: +# - url: your Azure OpenAI endpoint, e.g. https://my-resource.openai.azure.com +# - value: your Azure OpenAI API key +# +# Auth: upstream.auth is the gateway -> Azure vendor auth (header: api-key). +# Proxies in front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: azure-openai-provider +spec: + displayName: Azure OpenAI Provider + version: v1.0 + template: azure-openai + upstream: + url: #endpoint url + auth: + type: api-key + header: api-key + value: testkeyaddhere + policies: + - name: api-key-auth + version: v1 + paths: + - path: /openai/deployments/{deployment}/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /openai/deployments/{deployment}/chat/completions + methods: [POST] + - path: /openai/deployments/{deployment}/completions + methods: [POST] + - path: /openai/deployments/{deployment}/embeddings + methods: [POST] + - path: /openai/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: azure-openai-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: azure-openai-provider + displayName: Azure OpenAI provider loopback key + apiKey: + value: azure_openai_provider_loopback_key_1234567890abcdef + maskedApiKey: azure_openai_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/azure-openai-proxy.yaml b/gateway/examples/azure-openai-proxy.yaml new file mode 100644 index 0000000000..0466a62e72 --- /dev/null +++ b/gateway/examples/azure-openai-proxy.yaml @@ -0,0 +1,37 @@ +# -------------------------------------------------------------------- +# OpenAI -> Azure OpenAI LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-azure-openai` policy translates into Azure OpenAI's +# /openai/deployments/{deployment}/chat/completions?api-version=... +# format and forwards to the `azure-openai-provider` upstream. +# +# Single-provider mode: the policy omits the "provider" parameter, so +# it runs on every request without needing a setter policy. +# +# Replace before deploying: +# - apiVersion param: a valid Azure OpenAI api-version +# (e.g. 2024-02-15-preview) +# - model param (optional): pin the deployment id here, otherwise +# the request body's "model" field is used as the deployment. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-azure-openai +spec: + displayName: OpenAI to Azure OpenAI Proxy + version: v1.0 + context: /azure-openai + provider: + id: azure-openai-provider + policies: + - name: openai-to-azure-openai + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + apiVersion: "2024-02-15-preview" + model: gpt-4o diff --git a/gateway/examples/gemini-provider.yaml b/gateway/examples/gemini-provider.yaml new file mode 100644 index 0000000000..833bc962e5 --- /dev/null +++ b/gateway/examples/gemini-provider.yaml @@ -0,0 +1,58 @@ +# -------------------------------------------------------------------- +# Gemini LLM Provider Configuration +# +# Uses the built-in `gemini` template and points to Google's Gemini API. +# The openai-to-gemini policy on the proxy layer translates OpenAI-format +# requests into Gemini's generateContent API format. +# +# Auth: upstream.auth is the gateway -> Gemini vendor auth +# (header: x-goog-api-key). Proxies inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: gemini-provider +spec: + displayName: Gemini Provider + version: v1.0 + template: gemini + upstream: + url: https://generativelanguage.googleapis.com + auth: + type: api-key + header: x-goog-api-key + value: REPLACE_WITH_GEMINI_API_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1beta/models/{model}:generateContent + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1beta/models/{model}:generateContent + methods: [POST] + - path: /v1beta/models/{model}:streamGenerateContent + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: gemini-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: gemini-provider + displayName: Gemini provider loopback key + apiKey: + value: gemini_provider_loopback_key_1234567890abcdef + maskedApiKey: gemini_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/llm-provider.yaml b/gateway/examples/llm-provider.yaml index 7b4b8a7686..4585091617 100644 --- a/gateway/examples/llm-provider.yaml +++ b/gateway/examples/llm-provider.yaml @@ -31,6 +31,25 @@ spec: type: api-key header: Authorization value: api_key_abc123 + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - path: /models + methods: [GET] + params: + key: X-API-Key + in: header + - path: /models/{modelId} + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: @@ -40,3 +59,22 @@ spec: methods: [GET] - path: /models/{modelId} methods: [GET] + +--- +# Provider API key used by clients or by an LlmProxy provider.auth block. +# In production, prefer apiKey.valueFrom with a Kubernetes Secret. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: my-llm-provider-key +spec: + parentRef: + kind: LlmProvider + name: my-llm-provider + displayName: My LLM provider key + apiKey: + value: provider_loopback_key_1234567890abcdef1234567890 + maskedApiKey: provider_loopback_key_1234********7890 + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/llm-proxy.yaml b/gateway/examples/llm-proxy.yaml index fa3b6d5c02..5a78844a8c 100644 --- a/gateway/examples/llm-proxy.yaml +++ b/gateway/examples/llm-proxy.yaml @@ -26,6 +26,10 @@ spec: context: /assistant provider: id: my-llm-provider + auth: + type: api-key + header: X-API-Key + value: provider_loopback_key_1234567890abcdef1234567890 policies: - name: semanticPromptGuard version: v1 diff --git a/gateway/examples/mistral-openai-proxy.yaml b/gateway/examples/mistral-openai-proxy.yaml new file mode 100644 index 0000000000..17156d1068 --- /dev/null +++ b/gateway/examples/mistral-openai-proxy.yaml @@ -0,0 +1,43 @@ +# -------------------------------------------------------------------- +# OpenAI -> Mistral LLM Proxy (single-provider) +# +# Exposes an OpenAI-shaped /chat/completions endpoint that the +# `openai-to-mistral` policy adapts to Mistral's chat completions API +# and forwards to the existing `mistral-provider` upstream +# (api.mistral.ai). +# +# Single-provider mode: there is no router in front of the policy, so +# nothing sets metadata["selected_provider"] and the translator runs on +# every request. "id" is omitted, so it routes to the default upstream +# (the primary provider above). +# +# The provider must already be deployed and must have its Authorization +# bearer token set on upstream.auth (the policy does NOT inject the +# Mistral key). +# +# The provider example's api-key-auth policy expects the proxy to send the +# issued provider loopback key in X-API-Key. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-mistral +spec: + displayName: OpenAI to Mistral Proxy + version: v1.0 + context: /openai-mistral + provider: + id: mistral-provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + policies: + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest diff --git a/gateway/examples/mistral-provider.yaml b/gateway/examples/mistral-provider.yaml new file mode 100644 index 0000000000..67ec1c2654 --- /dev/null +++ b/gateway/examples/mistral-provider.yaml @@ -0,0 +1,63 @@ +# -------------------------------------------------------------------- +# Mistral LLM Provider Configuration +# +# Uses the built-in `mistralai` template and points to the public +# Mistral API endpoint. Replace the placeholder with a real Mistral key. +# +# The openai-to-mistral policy on the proxy layer translates +# OpenAI-format requests into Mistral's wire format (path +# /v1/chat/completions, model override, drop unsupported fields). +# +# Auth: upstream.auth is the gateway -> Mistral vendor auth. Proxies in +# front of this provider inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: mistral-provider +spec: + displayName: Mistral Provider + version: v1.0 + template: mistralai + upstream: + url: https://api.mistral.ai + auth: + type: api-key + header: Authorization + value: $dxxxd + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/chat/completions + methods: [POST] + - path: /v1/embeddings + methods: [POST] + - path: /v1/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: mistral-provider + displayName: Mistral provider loopback key + apiKey: + value: mistral_provider_loopback_key_1234567890abcdef + maskedApiKey: mistral_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/openai-multi-provider-proxy.yaml b/gateway/examples/openai-multi-provider-proxy.yaml new file mode 100644 index 0000000000..a8ba025970 --- /dev/null +++ b/gateway/examples/openai-multi-provider-proxy.yaml @@ -0,0 +1,151 @@ +# -------------------------------------------------------------------- +# OpenAI Multi-Provider Proxy +# +# Exposes one OpenAI-shaped /chat/completions endpoint and fans out to +# several LLM providers. Two modes are supported by the translators: +# +# * Single-provider mode — attach exactly one translator with no +# router in front of it. With no "selected_provider" in the request +# metadata the translator runs unconditionally. +# +# * Multi-provider mode — put openai-header-router first. It writes +# the chosen provider into metadata["selected_provider"]. Each +# translator then runs only when that selection equals its own "id". +# The "id" doubles as the upstream cluster name, so it must match an +# entry in additionalProviders (id or as). +# +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-multi +spec: + displayName: OpenAI Multi-Provider Proxy + version: v1.0 + context: /openai-multi + provider: + id: openai-provider + auth: + type: api-key + header: X-API-Key + value: openai_provider_loopback_key_1234567890abcdef + additionalProviders: + - id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + - id: azure-openai-provider + auth: + type: api-key + header: X-API-Key + value: azure_openai_provider_loopback_key_1234567890abcdef + - id: mistral-provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + #mistral_provider_loopback_key_1234567890abcdef + # - id: gemini-provider + # auth: + # type: api-key + # header: X-API-Key + # value: gemini_provider_loopback_key_1234567890abcdef + + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - name: openai-header-router + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + headerName: x-provider + defaultProvider: openai-provider + mappings: + - headerValue: openai + provider: openai-provider + - headerValue: anthropic + provider: anthropic-provider + - headerValue: azure-openai + provider: azure-openai-provider + - headerValue: mistral + provider: mistral-provider + # - headerValue: gemini + # provider: gemini-provider + + # - name: token-based-ratelimit + # version: v1 + # paths: + # - path: /chat/completions + # methods: [POST] + # params: + # totalTokenLimits: + # - count: 40 + # duration: "1m" + # algorithm: fixed-window + # backend: memory + + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 + id: anthropic-provider + + - name: openai-to-azure-openai + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + apiVersion: "2024-02-15-preview" + model: gpt-4o + id: azure-openai-provider + + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + id: mistral-provider + + # - name: openai-to-gemini + # version: v1 + # paths: + # - path: /chat/completions + # methods: [POST] + # params: + # model: gemini-2.5-flash + # id: gemini-provider + # apiVersion: v1beta + +--- +# Client API key for this proxy. The api-key-auth policy above validates +# the X-API-Key header against the key the gateway issues for this resource. +# apiKey is omitted here, so the gateway generates one. Read it from the +# create response (POST .../llm-proxies/openai-multi/api-keys). +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-multi-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-multi + displayName: OpenAI multi-proxy consumer key + expiresIn: + duration: 30 + unit: days diff --git a/gateway/examples/openai-provider.yaml b/gateway/examples/openai-provider.yaml new file mode 100644 index 0000000000..b206cff3f2 --- /dev/null +++ b/gateway/examples/openai-provider.yaml @@ -0,0 +1,67 @@ +# -------------------------------------------------------------------- +# OpenAI LLM Provider Configuration +# +# Uses the built-in `openai` template and points to the OpenAI public +# endpoint. The auth value below is a SAMPLE / placeholder token -- +# replace it with a real OpenAI API key (Bearer sk-...) before deploying. +# +# Auth: upstream.auth is the gateway -> OpenAI vendor auth. This is the +# only auth this provider needs; proxies in front of it route freely and +# inherit this vendor auth automatically. +# -------------------------------------------------------------------- + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: openai-provider +spec: + displayName: OpenAI Provider + version: v1.0 + template: openai + upstream: + url: https://api.openai.com/v1 + auth: + type: api-key + header: Authorization + # OpenAI expects: Authorization: Bearer -- sent verbatim, so the + # "Bearer " prefix MUST be included here. + value: Bearer sk-REPLACE_WITH_OPENAI_KEY + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /chat/completions + methods: [POST] + - path: /completions + methods: [POST] + - path: /embeddings + methods: [POST] + - path: /models + methods: [GET] + - path: /models/{modelId} + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: openai-provider + displayName: OpenAI provider loopback key + apiKey: + value: openai_provider_loopback_key_1234567890abcdef + maskedApiKey: openai_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days diff --git a/gateway/gateway-controller/api/management-openapi.yaml b/gateway/gateway-controller/api/management-openapi.yaml index 47c58fffb4..7dd022450a 100644 --- a/gateway/gateway-controller/api/management-openapi.yaml +++ b/gateway/gateway-controller/api/management-openapi.yaml @@ -5826,6 +5826,32 @@ components: auth: $ref: '#/components/schemas/LLMUpstreamAuth' + LLMProxyAdditionalProvider: + type: object + required: + - id + description: > + Additional LLM provider attached to this proxy as a selectable upstream. + Policies route to it by referring to the `as` name (defaults to `id`). + Optional auth config is used by the proxy when calling a protected + LlmProvider over the internal loopback route. + properties: + id: + type: string + description: Unique id of a deployed llm provider + example: anthropic-provider + as: + type: string + description: > + Logical upstream name used by policies to select this provider. Must + be unique within the proxy. Defaults to `id` when omitted. + pattern: '^[a-zA-Z0-9\-_]+$' + minLength: 1 + maxLength: 100 + example: anthropic-upstream + auth: + $ref: '#/components/schemas/LLMUpstreamAuth' + LLMAccessControl: type: object required: @@ -6009,6 +6035,15 @@ components: example: "api.openai.com" provider: $ref: '#/components/schemas/LLMProxyProvider' + additionalProviders: + type: array + description: > + Optional list of additional LLM providers attached to this proxy as + selectable upstreams. Policies (e.g. an OpenAI translator) can route + requests to any of these by setting the upstream name. The primary + `provider` field above remains the default upstream and the FK target. + items: + $ref: '#/components/schemas/LLMProxyAdditionalProvider' policies: type: array description: List of policies applied only to this operation (overrides or adds to API-level policies) diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index 4543d15129..2eb32544e0 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -917,8 +917,21 @@ type LLMProviderTemplateResourceMappings struct { Resources *[]LLMProviderTemplateResourceMapping `json:"resources,omitempty" yaml:"resources,omitempty"` } +// LLMProxyAdditionalProvider Additional LLM provider attached to this proxy as a selectable upstream. Policies route to it by referring to the `as` name (defaults to `id`). Optional auth config is used by the proxy when calling a protected LlmProvider over the internal loopback route. +type LLMProxyAdditionalProvider struct { + // As Logical upstream name used by policies to select this provider. Must be unique within the proxy. Defaults to `id` when omitted. + As *string `json:"as,omitempty" yaml:"as,omitempty"` + Auth *LLMUpstreamAuth `json:"auth,omitempty" yaml:"auth,omitempty"` + + // Id Unique id of a deployed llm provider + Id string `json:"id" yaml:"id"` +} + // LLMProxyConfigData defines model for LLMProxyConfigData. type LLMProxyConfigData struct { + // AdditionalProviders Optional list of additional LLM providers attached to this proxy as selectable upstreams. Policies (e.g. an OpenAI translator) can route requests to any of these by setting the upstream name. The primary `provider` field above remains the default upstream and the FK target. + AdditionalProviders *[]LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" yaml:"additionalProviders,omitempty"` + // Context Base path for all API routes (must start with /, no trailing slash) Context *string `json:"context,omitempty" yaml:"context,omitempty"` diff --git a/gateway/gateway-controller/pkg/config/llm_validator.go b/gateway/gateway-controller/pkg/config/llm_validator.go index 62fed822ac..cb44132b63 100644 --- a/gateway/gateway-controller/pkg/config/llm_validator.go +++ b/gateway/gateway-controller/pkg/config/llm_validator.go @@ -565,7 +565,82 @@ func (v *LLMValidator) validateProxyData(spec *api.LLMProxyConfigData) []Validat Message: "spec.provider.id must consist of lowercase alphanumeric characters, hyphens, or dots, and must start and end with an alphanumeric character", }) } + if spec.Provider.Auth != nil { + errors = append(errors, v.validateLLMUpstreamAuth("spec.provider.auth", spec.Provider.Auth)...) + } + + if spec.AdditionalProviders != nil { + seen := map[string]bool{spec.Provider.Id: true} + for i, provider := range *spec.AdditionalProviders { + fieldPrefix := fmt.Sprintf("spec.additionalProviders[%d]", i) + if provider.Id == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".id", + Message: "Provider is required", + }) + } else if !v.metadataNameRegex.MatchString(provider.Id) { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".id", + Message: fieldPrefix + ".id must consist of lowercase alphanumeric characters, hyphens, or dots, and must start and end with an alphanumeric character", + }) + } + + upstreamName := provider.Id + if provider.As != nil && *provider.As != "" { + upstreamName = *provider.As + if !regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString(upstreamName) { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".as", + Message: fieldPrefix + ".as must contain only letters, numbers, hyphens, or underscores", + }) + } + } + if upstreamName != "" { + if seen[upstreamName] { + errors = append(errors, ValidationError{ + Field: fieldPrefix, + Message: fmt.Sprintf("duplicate upstream name '%s' in additionalProviders", upstreamName), + }) + } + seen[upstreamName] = true + } + + if provider.Auth != nil { + errors = append(errors, v.validateLLMUpstreamAuth(fieldPrefix+".auth", provider.Auth)...) + } + } + } + + return errors +} +func (v *LLMValidator) validateLLMUpstreamAuth(fieldPrefix string, auth *api.LLMUpstreamAuth) []ValidationError { + var errors []ValidationError + if auth.Type == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".type", + Message: "Auth type is required", + }) + } else if auth.Type != api.LLMUpstreamAuthTypeApiKey { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".type", + Message: "Auth type must be 'api-key'", + }) + } + if auth.Type == api.LLMUpstreamAuthTypeApiKey { + if auth.Header == nil || *auth.Header == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".header", + Message: "Auth header is required when api-key auth type is set", + }) + } + if auth.Value == nil || *auth.Value == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".value", + Message: "Auth value is required when api-key auth type is set", + }) + } + } return errors } diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer.go b/gateway/gateway-controller/pkg/utils/llm_transformer.go index 3c4d5edf30..701114746c 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer.go @@ -148,6 +148,50 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration spec.Upstream.Main = api.Upstream{ Url: &upstream, } + + // Step 3.1: Resolve additional providers (multi-provider proxies). Each is + // exposed as a named UpstreamDefinition so policies can route to it via + // the loopback context. The primary provider above remains the default. + if proxy.Spec.AdditionalProviders != nil && len(*proxy.Spec.AdditionalProviders) > 0 { + seen := map[string]bool{proxy.Spec.Provider.Id: true} + var defs []api.UpstreamDefinition + for _, ap := range *proxy.Spec.AdditionalProviders { + if ap.Id == "" { + return nil, fmt.Errorf("additionalProviders entry must have a non-empty id") + } + name := ap.Id + if ap.As != nil && *ap.As != "" { + name = *ap.As + } + if seen[name] { + return nil, fmt.Errorf("duplicate upstream name '%s' in additionalProviders (must be unique within the proxy and not collide with the primary provider id)", name) + } + seen[name] = true + + addCfg, err := t.db.GetConfigByKindAndHandle(string(api.LLMProviderConfigurationKindLlmProvider), ap.Id) + if err != nil { + return nil, fmt.Errorf("failed to look up additional provider '%s': %w", ap.Id, err) + } + if addCfg == nil { + return nil, fmt.Errorf("additional provider '%s' not found", ap.Id) + } + addCtx, err := addCfg.GetContext() + if err != nil { + return nil, fmt.Errorf("failed to get context for additional provider '%s': %w", ap.Id, err) + } + addURL := fmt.Sprintf("%s://%s:%d%s", + constants.SchemeHTTP, constants.LocalhostIP, t.routerConfig.ListenerPort, addCtx) + defs = append(defs, api.UpstreamDefinition{ + Name: name, + Upstreams: []struct { + Url string `json:"url" yaml:"url"` + Weight *int `json:"weight,omitempty" yaml:"weight,omitempty"` + }{{Url: addURL}}, + }) + } + spec.UpstreamDefinitions = &defs + } + // If provider has vhost configured add a host adding policy if providerConfig.Spec.Vhost != nil && *providerConfig.Spec.Vhost != "" { providerVhost := *providerConfig.Spec.Vhost @@ -182,34 +226,32 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration } // Step 3.5: Apply proxy-level provider auth for proxy->provider loopback upstream - var upstreamAuthPolicy *api.Policy + var upstreamAuthPolicies []api.Policy if proxy.Spec.Provider.Auth != nil { - auth := proxy.Spec.Provider.Auth - switch auth.Type { - case api.LLMUpstreamAuthTypeApiKey: - if auth.Value == nil || *auth.Value == "" { - return nil, fmt.Errorf("provider.auth.value is required") - } - header := "" - if auth.Header != nil { - header = *auth.Header + pol, err := t.proxyUpstreamAuthPolicy(proxy.Spec.Provider.Auth, "provider.auth") + if err != nil { + return nil, err + } + condition := selectedProviderExecutionCondition(proxy.Spec.Provider.Id, true) + pol.ExecutionCondition = &condition + upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) + } + if proxy.Spec.AdditionalProviders != nil { + for _, ap := range *proxy.Spec.AdditionalProviders { + if ap.Auth == nil { + continue } - params, err := GetUpstreamAuthApikeyPolicyParams(header, *auth.Value) - if err != nil { - return nil, fmt.Errorf("failed to build upstream auth params: %w", err) + name := ap.Id + if ap.As != nil && *ap.As != "" { + name = *ap.As } - policyVersion, err := t.resolvePolicyVersion(constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME) + pol, err := t.proxyUpstreamAuthPolicy(ap.Auth, fmt.Sprintf("additionalProviders[%s].auth", name)) if err != nil { return nil, err } - mh := api.Policy{ - Name: constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME, - Version: policyVersion, - Params: ¶ms, - } - upstreamAuthPolicy = &mh - default: - return nil, fmt.Errorf("unsupported upstream auth type: %s", auth.Type) + condition := selectedProviderExecutionCondition(name, false) + pol.ExecutionCondition = &condition + upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) } } @@ -282,14 +324,10 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration ops = append(ops, *op) } ops = sortOperationsBySpecificity(ops) - if upstreamAuthPolicy != nil { + if len(upstreamAuthPolicies) > 0 { for i := range ops { - if ops[i].Policies == nil { - ops[i].Policies = &[]api.Policy{*upstreamAuthPolicy} - } else { - existing := *ops[i].Policies - existing = append(existing, *upstreamAuthPolicy) - ops[i].Policies = &existing + for _, upstreamAuthPolicy := range upstreamAuthPolicies { + appendOperationPolicy(&ops[i], upstreamAuthPolicy) } } } @@ -625,6 +663,44 @@ func GetUpstreamAuthApikeyPolicyParams(header, value string) (map[string]interfa return m, nil } +func (t *LLMProviderTransformer) proxyUpstreamAuthPolicy(auth *api.LLMUpstreamAuth, field string) (*api.Policy, error) { + if auth == nil { + return nil, nil + } + switch auth.Type { + case api.LLMUpstreamAuthTypeApiKey: + if auth.Header == nil || *auth.Header == "" { + return nil, fmt.Errorf("%s.header is required", field) + } + if auth.Value == nil || *auth.Value == "" { + return nil, fmt.Errorf("%s.value is required", field) + } + params, err := GetUpstreamAuthApikeyPolicyParams(*auth.Header, *auth.Value) + if err != nil { + return nil, fmt.Errorf("failed to build upstream auth params: %w", err) + } + policyVersion, err := t.resolvePolicyVersion(constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME) + if err != nil { + return nil, err + } + return &api.Policy{ + Name: constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME, + Version: policyVersion, + Params: ¶ms, + }, nil + default: + return nil, fmt.Errorf("unsupported upstream auth type: %s", auth.Type) + } +} + +func selectedProviderExecutionCondition(providerName string, includeDefault bool) string { + selectedExpr := fmt.Sprintf("request.Metadata['selected_provider'] == '%s'", providerName) + if includeDefault { + return fmt.Sprintf("!('selected_provider' in request.Metadata) || %s", selectedExpr) + } + return fmt.Sprintf("'selected_provider' in request.Metadata && %s", selectedExpr) +} + // GetHostAdditionPolicyParams renders the policy params with given host value (host-rewrite) func GetHostAdditionPolicyParams(value string) (map[string]interface{}, error) { rendered := fmt.Sprintf(constants.PROXY_HOST__HEADER_POLICY_PARAMS, value) diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go index 061bf2952a..5bc2aaad8a 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go @@ -200,6 +200,119 @@ func TestLLMProviderTransformer_TransformProxy_ReadsProviderAndTemplateFromDB(t assert.Equal(t, "http://127.0.0.1:8080/db-provider", *result.Spec.Upstream.Main.Url) } +func TestLLMProviderTransformer_TransformProxy_AdditionalProviderAuthIsConditional(t *testing.T) { + store := storage.NewConfigStore() + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + db := newTestSQLiteStorage(t, logger) + + template := &models.StoredLLMProviderTemplate{ + UUID: "0000-db-template-id-0000-000000000002", + Configuration: api.LLMProviderTemplate{ + ApiVersion: api.LLMProviderTemplateApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderTemplateKindLlmProviderTemplate, + Metadata: api.Metadata{Name: "openai"}, + Spec: api.LLMProviderTemplateData{DisplayName: "openai"}, + }, + } + require.NoError(t, db.SaveLLMProviderTemplate(template)) + + saveProvider := func(name, context string) { + providerSourceConfig := api.LLMProviderConfiguration{ + ApiVersion: api.LLMProviderConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderConfigurationKindLlmProvider, + Metadata: api.Metadata{Name: name}, + Spec: api.LLMProviderConfigData{ + DisplayName: name, + Version: "v1.0", + Context: stringPtr(context), + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://example.com")}, + AccessControl: api.LLMAccessControl{Mode: api.AllowAll}, + }, + } + require.NoError(t, db.SaveConfig(&models.StoredConfig{ + UUID: name + "-uuid", + Kind: string(api.LLMProviderConfigurationKindLlmProvider), + Handle: name, + DisplayName: name, + Version: "v1.0", + SourceConfiguration: providerSourceConfig, + DesiredState: models.StateDeployed, + })) + } + saveProvider("openai-provider", "/openai-provider") + saveProvider("anthropic-provider", "/anthropic-provider") + + transformer := NewLLMProviderTransformer(store, db, &config.RouterConfig{ListenerPort: 8080}, newTestPolicyVersionResolver()) + + proxy := &api.LLMProxyConfiguration{ + ApiVersion: api.LLMProxyConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProxyConfigurationKindLlmProxy, + Metadata: api.Metadata{Name: "openai-multi"}, + Spec: api.LLMProxyConfigData{ + DisplayName: "openai-multi", + Version: "v1.0", + Provider: api.LLMProxyProvider{ + Id: "openai-provider", + Auth: &api.LLMUpstreamAuth{ + Type: api.LLMUpstreamAuthTypeApiKey, + Header: stringPtr("Authorization"), + Value: stringPtr("Bearer primary"), + }, + }, + AdditionalProviders: &[]api.LLMProxyAdditionalProvider{{ + Id: "anthropic-provider", + Auth: &api.LLMUpstreamAuth{ + Type: api.LLMUpstreamAuthTypeApiKey, + Header: stringPtr("X-Provider-Key"), + Value: stringPtr("anthropic-loopback"), + }, + }}, + Policies: &[]api.LLMPolicy{{ + Name: "openai-header-router", + Version: "v1", + Paths: []api.LLMPolicyPath{{ + Path: "/chat/completions", + Methods: []api.LLMPolicyPathMethods{"POST"}, + Params: map[string]interface{}{ + "defaultProvider": "openai-provider", + }, + }}, + }}, + }, + } + + result, err := transformer.Transform(proxy, &api.RestAPI{}) + require.NoError(t, err) + require.NotNil(t, result.Spec.UpstreamDefinitions) + require.Len(t, *result.Spec.UpstreamDefinitions, 1) + assert.Equal(t, "anthropic-provider", (*result.Spec.UpstreamDefinitions)[0].Name) + + var chatOp *api.Operation + for i := range result.Spec.Operations { + if result.Spec.Operations[i].Path == "/chat/completions" && result.Spec.Operations[i].Method == "POST" { + chatOp = &result.Spec.Operations[i] + break + } + } + require.NotNil(t, chatOp) + require.NotNil(t, chatOp.Policies) + + var authPolicies []api.Policy + for _, pol := range *chatOp.Policies { + if pol.Name == constants.UPSTREAM_AUTH_APIKEY_POLICY_NAME { + authPolicies = append(authPolicies, pol) + } + } + require.Len(t, authPolicies, 2) + require.NotNil(t, authPolicies[0].ExecutionCondition) + require.NotNil(t, authPolicies[1].ExecutionCondition) + assert.Contains(t, *authPolicies[0].ExecutionCondition, "openai-provider") + assert.Contains(t, *authPolicies[1].ExecutionCondition, "anthropic-provider") + assert.Equal(t, "Bearer primary", firstRequestHeaderValue(t, authPolicies[0].Params)) + assert.Equal(t, "anthropic-loopback", firstRequestHeaderValue(t, authPolicies[1].Params)) +} + func TestGetUpstreamAuthApikeyPolicyParams_Extended(t *testing.T) { t.Run("Valid parameters", func(t *testing.T) { params, err := GetUpstreamAuthApikeyPolicyParams("Authorization", "Bearer token123") @@ -215,6 +328,21 @@ func TestGetUpstreamAuthApikeyPolicyParams_Extended(t *testing.T) { }) } +func firstRequestHeaderValue(t *testing.T, params *map[string]interface{}) string { + t.Helper() + require.NotNil(t, params) + request, ok := (*params)["request"].(map[string]interface{}) + require.True(t, ok) + headers, ok := request["headers"].([]interface{}) + require.True(t, ok) + require.NotEmpty(t, headers) + header, ok := headers[0].(map[string]interface{}) + require.True(t, ok) + value, ok := header["value"].(string) + require.True(t, ok) + return value +} + func TestGetHostAdditionPolicyParams(t *testing.T) { t.Run("Valid host value", func(t *testing.T) { params, err := GetHostAdditionPolicyParams("api.example.com") diff --git a/gateway/sample-policies/slugify-body/policy-definition.yaml b/gateway/sample-policies/slugify-body/policy-definition.yaml index f19a90c78c..68882e2e99 100644 --- a/gateway/sample-policies/slugify-body/policy-definition.yaml +++ b/gateway/sample-policies/slugify-body/policy-definition.yaml @@ -27,22 +27,9 @@ name: slugify-body version: v1.0.0 displayName: Slugify Body description: | - Extracts a string value from the JSON request body at the configured JSONPath, - converts it into a URL-friendly slug using the `python-slugify` library, and - writes the slug back into the payload before forwarding to the upstream - service. - - Typical use cases include: - - Generating URL slugs from user-supplied titles or names. - - Normalising free-text identifiers into safe, lowercase, ASCII strings. - - Cleaning up input data at the gateway layer before it reaches backends. - - This is a **sample policy** that demonstrates: - 1. Using a third-party PyPI dependency (`python-slugify`). - 2. Reading and modifying JSON request bodies (buffered mode). - 3. Declaring configurable parameters with JSON Schema validation. - 4. Returning modified request bodies via `UpstreamRequestModifications`. - 5. Returning early error responses via `ImmediateResponse`. + Extracts a string from the JSON request body, converts it into a URL-friendly + slug, and writes the slug back before forwarding upstream. Demonstrates a + Python policy with third-party dependencies and JSON Schema parameters. # --- User-configurable parameters -------------------------------------------- # The `parameters` section is a full JSON Schema object. Every key under diff --git a/gateway/system-policies/analytics/policy-definition.yaml b/gateway/system-policies/analytics/policy-definition.yaml index 7fe915469d..1a57a8bf06 100644 --- a/gateway/system-policies/analytics/policy-definition.yaml +++ b/gateway/system-policies/analytics/policy-definition.yaml @@ -1,10 +1,9 @@ name: wso2_apip_sys_analytics version: v1.0.0 description: | - Collects analytics data from request and response flows. This gets attached even - without any policies in the chain. Inspects headers and, optionally, bodies to - populate analytics metadata that is emitted via Envoy dynamic metadata and consumed - by the policy engine analytics pipeline and publishers. + Collects request and response analytics metadata from gateway traffic, + including optional payload capture, for the policy engine analytics pipeline + and publishers. parameters: type: object diff --git a/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go b/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go index 046f492af5..b4b29e2ee4 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go +++ b/kubernetes/gateway-operator/api/v1alpha1/llmproxy_types.go @@ -32,6 +32,23 @@ type LLMProxyProvider struct { Auth *LLMUpstreamAuth `json:"auth,omitempty"` } +// LLMProxyAdditionalProvider references an additional LlmProvider that this +// proxy can route to by policy-selected upstream name. +type LLMProxyAdditionalProvider struct { + // Id is the LlmProvider handle (metadata.name). + // +kubebuilder:validation:Required + Id string `json:"id"` + + // As is the logical upstream name used by policies. Defaults to Id. + // +optional + As *string `json:"as,omitempty"` + + // Auth optionally configures credentials for proxy-to-provider loopback + // calls when the referenced provider is protected by an auth policy. + // +optional + Auth *LLMUpstreamAuth `json:"auth,omitempty"` +} + // LLMProxyConfigData mirrors the management-API LLMProxyConfigData payload. type LLMProxyConfigData struct { // DisplayName is a human-readable LLM proxy name. @@ -47,6 +64,11 @@ type LLMProxyConfigData struct { // +kubebuilder:validation:Required Provider LLMProxyProvider `json:"provider"` + // AdditionalProviders are extra LLM providers attached as selectable + // upstreams for multi-provider routing. + // +optional + AdditionalProviders []LLMProxyAdditionalProvider `json:"additionalProviders,omitempty"` + // Context is the base path for routes (must start with /). // +optional // +kubebuilder:validation:Pattern=`^/[a-zA-Z0-9\-._~!$&'()*+,;=:@%/]*[^/]$` diff --git a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go index b6163d5fd5..cfeda2ef43 100644 --- a/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/kubernetes/gateway-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -965,6 +965,13 @@ func (in *LLMProviderUpstream) DeepCopy() *LLMProviderUpstream { func (in *LLMProxyConfigData) DeepCopyInto(out *LLMProxyConfigData) { *out = *in in.Provider.DeepCopyInto(&out.Provider) + if in.AdditionalProviders != nil { + in, out := &in.AdditionalProviders, &out.AdditionalProviders + *out = make([]LLMProxyAdditionalProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Context != nil { in, out := &in.Context, &out.Context *out = new(string) @@ -999,6 +1006,31 @@ func (in *LLMProxyConfigData) DeepCopy() *LLMProxyConfigData { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LLMProxyAdditionalProvider) DeepCopyInto(out *LLMProxyAdditionalProvider) { + *out = *in + if in.As != nil { + in, out := &in.As, &out.As + *out = new(string) + **out = **in + } + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(LLMUpstreamAuth) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LLMProxyAdditionalProvider. +func (in *LLMProxyAdditionalProvider) DeepCopy() *LLMProxyAdditionalProvider { + if in == nil { + return nil + } + out := new(LLMProxyAdditionalProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LLMProxyProvider) DeepCopyInto(out *LLMProxyProvider) { *out = *in diff --git a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml index dd5a2a5579..db0259fde6 100644 --- a/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml +++ b/kubernetes/gateway-operator/config/crd/bases/gateway.api-platform.wso2.com_llmproxies.yaml @@ -55,6 +55,85 @@ spec: displayName: description: DisplayName is a human-readable LLM proxy name. type: string + additionalProviders: + description: |- + AdditionalProviders are extra LLM providers attached as selectable + upstreams for multi-provider routing. + items: + description: |- + LLMProxyAdditionalProvider references an additional LlmProvider that this + proxy can route to by policy-selected upstream name. + properties: + as: + description: As is the logical upstream name used by policies. + Defaults to Id. + type: string + auth: + description: |- + Auth optionally configures credentials for proxy-to-provider loopback + calls when the referenced provider is protected by an auth policy. + properties: + header: + description: |- + Header is the HTTP header to set on outbound requests when the auth + type uses headers (e.g. api-key). + type: string + type: + description: Type identifies the auth scheme. Only api-key + is currently supported. + enum: + - api-key + type: string + value: + description: |- + Value sources the credential. Exactly one of value or valueFrom must + be set. + properties: + value: + description: |- + Value is the inline plaintext value. Avoid for production use; prefer + valueFrom. + type: string + valueFrom: + description: ValueFrom references a Kubernetes Secret key. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: exactly one of value or valueFrom must be + set + rule: has(self.value) != has(self.valueFrom) + required: + - type + - value + type: object + id: + description: Id is the LlmProvider handle (metadata.name). + type: string + required: + - id + type: object + type: array policies: description: Policies is the list of policies applied to this LLM proxy. diff --git a/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go b/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go index 987b84a056..2d68ea5986 100644 --- a/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go +++ b/kubernetes/gateway-operator/internal/controller/llmproxy_controller.go @@ -159,20 +159,38 @@ func (a *llmProxyAdapter) Deploy(ctx context.Context, k8sClient client.Client, g } specPayload := interface{}(spec) - if spec.Provider.Auth != nil { - resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.Provider.Auth, "spec.provider.auth.value") - if err != nil { - return DeployResult{}, err - } - if resolved == nil { - return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: provider.auth resolution produced nil")} - } + if spec.Provider.Auth != nil || len(spec.AdditionalProviders) > 0 { m, err := specToJSONMap(spec) if err != nil { return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} } - if err := flattenUpstreamAuthCredentialValue(m, "provider", *resolved); err != nil { - return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + if spec.Provider.Auth != nil { + resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.Provider.Auth, "spec.provider.auth.value") + if err != nil { + return DeployResult{}, err + } + if resolved == nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: provider.auth resolution produced nil")} + } + if err := flattenUpstreamAuthCredentialValue(m, "provider", *resolved); err != nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + } + } + for i := range spec.AdditionalProviders { + if spec.AdditionalProviders[i].Auth == nil { + continue + } + fieldPath := fmt.Sprintf("spec.additionalProviders[%d].auth.value", i) + resolved, err := resolveLLMProviderUpstreamAuth(ctx, k8sClient, cr.Namespace, spec.AdditionalProviders[i].Auth, fieldPath) + if err != nil { + return DeployResult{}, err + } + if resolved == nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: fmt.Errorf("internal: additionalProviders[%d].auth resolution produced nil", i)} + } + if err := flattenAdditionalProviderAuthCredentialValue(m, i, *resolved); err != nil { + return DeployResult{}, &gatewayclient.NonRetryableError{Err: err} + } } specPayload = m } diff --git a/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go b/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go index 6703f9c3dd..f6cec1c13c 100644 --- a/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go +++ b/kubernetes/gateway-operator/internal/controller/llmproxy_controller_test.go @@ -93,3 +93,85 @@ func TestLlmProxyDeploy_ResolvesPolicyParamsValueFrom(t *testing.T) { require.True(t, strings.Contains(got, "name: api-key-auth")) } +func TestLlmProxyDeploy_ResolvesAdditionalProviderAuthValueFrom(t *testing.T) { + scheme := runtime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(apiv1.AddToScheme(scheme)) + + sec := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: "apigateway-demo", Name: "provider-secrets"}, + Data: map[string][]byte{ + "primary": []byte("Bearer primary-key"), + "additional": []byte("Bearer additional-key"), + }, + } + k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(sec).Build() + + header := "Authorization" + authType := "api-key" + cr := &apiv1.LlmProxy{ + ObjectMeta: metav1.ObjectMeta{Name: "demo-llm-proxy", Namespace: "apigateway-demo"}, + Spec: apiv1.LLMProxyConfigData{ + DisplayName: "Lifecycle LLM Proxy", + Version: "v1.0", + Provider: apiv1.LLMProxyProvider{ + Id: "openai-provider", + Auth: &apiv1.LLMUpstreamAuth{ + Type: authType, + Header: &header, + Value: apiv1.SecretValueSource{ValueFrom: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "provider-secrets"}, + Key: "primary", + }}, + }, + }, + AdditionalProviders: []apiv1.LLMProxyAdditionalProvider{{ + Id: "anthropic-provider", + Auth: &apiv1.LLMUpstreamAuth{ + Type: authType, + Header: &header, + Value: apiv1.SecretValueSource{ValueFrom: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "provider-secrets"}, + Key: "additional", + }}, + }, + }}, + }, + } + + var ( + mu sync.Mutex + payload string + ) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + if r.Method == http.MethodPost { + b, _ := io.ReadAll(r.Body) + mu.Lock() + payload = string(b) + mu.Unlock() + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("{}")) + return + } + w.WriteHeader(http.StatusMethodNotAllowed) + })) + defer srv.Close() + + adapter := &llmProxyAdapter{} + _, err := adapter.Deploy(context.Background(), k8sClient, srv.URL, cr, nil) + require.NoError(t, err) + + mu.Lock() + got := payload + mu.Unlock() + require.Contains(t, got, "value: Bearer primary-key") + require.Contains(t, got, "value: Bearer additional-key") + require.Contains(t, got, "additionalProviders:") + require.NotContains(t, got, "secretKeyRef") + require.NotContains(t, got, "valueFrom") +} diff --git a/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go b/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go index e32c01e772..ffe5f609f9 100644 --- a/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go +++ b/kubernetes/gateway-operator/internal/controller/management_upstream_auth_payload.go @@ -42,3 +42,23 @@ func flattenUpstreamAuthCredentialValue(specMap map[string]interface{}, parentKe auth["value"] = plaintext return nil } + +func flattenAdditionalProviderAuthCredentialValue(specMap map[string]interface{}, index int, plaintext string) error { + additionalProviders, ok := specMap["additionalProviders"].([]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders must be an array") + } + if index < 0 || index >= len(additionalProviders) { + return fmt.Errorf("spec.additionalProviders[%d] is out of range", index) + } + parent, ok := additionalProviders[index].(map[string]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders[%d] must be an object", index) + } + auth, ok := parent["auth"].(map[string]interface{}) + if !ok { + return fmt.Errorf("spec.additionalProviders[%d].auth must be an object", index) + } + auth["value"] = plaintext + return nil +} diff --git a/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go b/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go index 92e26fe455..7760063f47 100644 --- a/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go +++ b/kubernetes/gateway-operator/internal/controller/management_valuefrom_enqueue.go @@ -218,6 +218,14 @@ func llmProxyReferencesSecret(cr *apiv1.LlmProxy, secretNS, secretName string) b return true } } + for i := range cr.Spec.AdditionalProviders { + auth := cr.Spec.AdditionalProviders[i].Auth + if auth != nil && auth.Value.ValueFrom != nil { + if cr.Namespace == secretNS && strings.TrimSpace(auth.Value.ValueFrom.Name) == secretName { + return true + } + } + } return llmProxyReferencesValueFromKind(cr, secretKeyRefKey, secretNS, secretName) } @@ -240,4 +248,3 @@ func llmProxyReferencesValueFromKind(cr *apiv1.LlmProxy, kind, targetNS, targetN } return false } - diff --git a/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go b/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go index 2c6a19068a..16bd4a42dd 100644 --- a/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go +++ b/kubernetes/gateway-operator/internal/controller/management_valuefrom_fingerprint.go @@ -110,6 +110,11 @@ func llmProxyExternalDepsFingerprint(ctx context.Context, c client.Client, cr *a if cr.Spec.Provider.Auth != nil { accumulateBackingFromSecretValueSource(cr.Spec.Provider.Auth.Value, cr.Namespace, backing) } + for i := range cr.Spec.AdditionalProviders { + if cr.Spec.AdditionalProviders[i].Auth != nil { + accumulateBackingFromSecretValueSource(cr.Spec.AdditionalProviders[i].Auth.Value, cr.Namespace, backing) + } + } for i := range cr.Spec.Policies { for j := range cr.Spec.Policies[i].Paths { if p := cr.Spec.Policies[i].Paths[j].Params; p != nil { @@ -139,4 +144,3 @@ func resolveRawExtensionValueFrom(ctx context.Context, c client.Client, defaultN raw.Raw = out return nil } - diff --git a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml index dd5a2a5579..db0259fde6 100644 --- a/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml +++ b/kubernetes/helm/operator-helm-chart/crds/gateway.api-platform.wso2.com_llmproxies.yaml @@ -55,6 +55,85 @@ spec: displayName: description: DisplayName is a human-readable LLM proxy name. type: string + additionalProviders: + description: |- + AdditionalProviders are extra LLM providers attached as selectable + upstreams for multi-provider routing. + items: + description: |- + LLMProxyAdditionalProvider references an additional LlmProvider that this + proxy can route to by policy-selected upstream name. + properties: + as: + description: As is the logical upstream name used by policies. + Defaults to Id. + type: string + auth: + description: |- + Auth optionally configures credentials for proxy-to-provider loopback + calls when the referenced provider is protected by an auth policy. + properties: + header: + description: |- + Header is the HTTP header to set on outbound requests when the auth + type uses headers (e.g. api-key). + type: string + type: + description: Type identifies the auth scheme. Only api-key + is currently supported. + enum: + - api-key + type: string + value: + description: |- + Value sources the credential. Exactly one of value or valueFrom must + be set. + properties: + value: + description: |- + Value is the inline plaintext value. Avoid for production use; prefer + valueFrom. + type: string + valueFrom: + description: ValueFrom references a Kubernetes Secret key. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: exactly one of value or valueFrom must be + set + rule: has(self.value) != has(self.valueFrom) + required: + - type + - value + type: object + id: + description: Id is the LlmProvider handle (metadata.name). + type: string + required: + - id + type: object + type: array policies: description: Policies is the list of policies applied to this LLM proxy. diff --git a/mistral-proxy.yaml b/mistral-proxy.yaml new file mode 100644 index 0000000000..536a17c5b8 --- /dev/null +++ b/mistral-proxy.yaml @@ -0,0 +1,246 @@ +# ==================================================================== +# Mistral — Provider + Single Proxy + Consumer Key (docs 1-4) +# Plus a MULTI-PROVIDER proxy sample at the bottom (docs 5-6) +# +# THREE auth hops exist — keep them straight: +# (A) provider.upstream.auth -> provider -> Mistral (the Mistral key) +# (B) proxy api-key-auth -> client -> proxy (consumer key) +# (C) proxy.provider.auth -> proxy -> provider (loopback, optional) +# +# For every auth block: type MUST be "api-key" (never "x-api-key"); +# "x-api-key" is only ever a HEADER NAME, never a type. value non-empty. +# ==================================================================== + +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: mistral-provider +spec: + displayName: Mistral Provider + version: v1.0 + template: mistralai + upstream: + url: https://api.mistral.ai + auth: + type: api-key # (A) only valid type for an LlmProvider + header: Authorization # Mistral expects: Authorization: Bearer + value: Bearer # <-- put your real Mistral key here + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/chat/completions + methods: [POST] + - path: /v1/embeddings + methods: [POST] + - path: /v1/models + methods: [GET] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: mistral-provider + displayName: Mistral provider loopback key + apiKey: + value: mistral_provider_loopback_key_1234567890abcdef + maskedApiKey: mistral_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days + +--- +# Second provider used only by the MULTI-PROVIDER sample below. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProvider +metadata: + name: anthropic-provider +spec: + displayName: Anthropic Provider + version: v1.0 + template: anthropic + upstream: + url: https://api.anthropic.com + auth: + type: api-key # type is still "api-key"... + header: x-api-key # ...but Anthropic's HEADER is x-api-key (no Bearer) + value: + policies: + - name: api-key-auth + version: v1 + paths: + - path: /v1/messages + methods: [POST] + params: + key: X-API-Key + in: header + accessControl: + mode: deny_all + exceptions: + - path: /v1/messages + methods: [POST] + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: anthropic-provider-loopback-key +spec: + parentRef: + kind: LlmProvider + name: anthropic-provider + displayName: Anthropic provider loopback key + apiKey: + value: anthropic_provider_loopback_key_1234567890abcdef + maskedApiKey: anthropic_provider_loopback_key_1234********cdef + expiresIn: + duration: 30 + unit: days + +--- +# ----- SINGLE-provider proxy ----------------------------------------- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-to-mistral +spec: + displayName: OpenAI to Mistral Proxy + version: v1.0 + context: /openai-mistral + provider: + id: mistral-provider + # (C) OPTIONAL proxy->provider loopback auth. A SEPARATE hop, NOT the + # Mistral key. Only needed if the provider API itself is secured. + # Corrected form: type "api-key" (NOT "x-api-key"), value non-empty. + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + policies: + - name: api-key-auth # (B) client -> proxy consumer key + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + +--- +# Consumer key for the single proxy. spec.apiKey omitted -> gateway generates it. +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: mistral-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-to-mistral + displayName: Mistral proxy consumer key + expiresIn: + duration: 30 + unit: days + +--- +# ----- MULTI-provider proxy sample ----------------------------------- +# One OpenAI-shaped endpoint that fans out to Mistral OR Anthropic based +# on the `x-provider` request header (read by the openai-header-router +# policy). Each provider has its own LlmProvider ApiKey above; the proxy +# sends those loopback keys via provider.auth/additionalProviders[].auth. +# +# Routing: header x-provider: mistral -> mistral-provider +# header x-provider: anthropic -> anthropic-provider +# (absent/unmatched) -> defaultProvider (mistral) +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: LlmProxy +metadata: + name: openai-multi +spec: + displayName: OpenAI Multi-Provider Proxy + version: v1.0 + context: /openai-multi + provider: + id: mistral-provider # primary/default provider + auth: + type: api-key + header: X-API-Key + value: mistral_provider_loopback_key_1234567890abcdef + additionalProviders: + - id: anthropic-provider + auth: + type: api-key + header: X-API-Key + value: anthropic_provider_loopback_key_1234567890abcdef + policies: + - name: api-key-auth + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + key: X-API-Key + in: header + # 1) Selector: reads x-provider header, picks the provider. + - name: openai-header-router + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + headerName: x-provider + defaultProvider: mistral-provider + mappings: + - headerValue: mistral + provider: mistral-provider + - headerValue: anthropic + provider: anthropic-provider + # 2) Per-provider translators. Each runs only when its id is selected. + - name: openai-to-mistral + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: mistral-large-latest + id: mistral-provider + - name: openai-to-anthropic + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: claude-sonnet-4-5-20250929 + id: anthropic-provider + +--- +apiVersion: gateway.api-platform.wso2.com/v1alpha1 +kind: ApiKey +metadata: + name: openai-multi-consumer-key +spec: + parentRef: + kind: LlmProxy + name: openai-multi + displayName: OpenAI multi-proxy consumer key + expiresIn: + duration: 30 + unit: days diff --git a/platform-api/src/api/generated.go b/platform-api/src/api/generated.go index 2420211e64..05a3d25c8a 100644 --- a/platform-api/src/api/generated.go +++ b/platform-api/src/api/generated.go @@ -1812,6 +1812,9 @@ type LLMProviderTemplateResourceMappings struct { // LLMProxy defines model for LLMProxy. type LLMProxy struct { + // AdditionalProviders Optional list of additional LLM providers attached to this proxy as selectable upstreams. Policies route requests to any of these by setting the upstream name. The primary `provider` field above remains the default upstream and the FK target. + AdditionalProviders *[]LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" yaml:"additionalProviders,omitempty"` + // Context Base path for all REST API routes (must start with /, no trailing slash) Context *string `json:"context,omitempty" yaml:"context,omitempty"` @@ -1862,6 +1865,15 @@ type LLMProxyAPIKeyListResponse struct { Items []APIKeyItem `binding:"required" json:"items" yaml:"items"` } +// LLMProxyAdditionalProvider Additional LLM provider attached to this proxy as a selectable upstream. Policies route to it by referring to the `as` name (defaults to `id`). +type LLMProxyAdditionalProvider struct { + // As Logical upstream name used by policies to select this provider. Must be unique within the proxy. Defaults to `id` when omitted. + As *string `json:"as,omitempty" yaml:"as,omitempty"` + + // Id Unique id of a deployed llm provider + Id string `binding:"required" json:"id" yaml:"id"` +} + // LLMProxyListItem defines model for LLMProxyListItem. type LLMProxyListItem struct { // Context Context path where the proxy is exposed diff --git a/platform-api/src/internal/dto/llm_deployment.go b/platform-api/src/internal/dto/llm_deployment.go index 9fc0c7a8a6..f8891ee962 100644 --- a/platform-api/src/internal/dto/llm_deployment.go +++ b/platform-api/src/internal/dto/llm_deployment.go @@ -59,15 +59,21 @@ type LLMProxyDeploymentYAML struct { // LLMProxyDeploymentSpec represents the spec section for LLM proxy deployments type LLMProxyDeploymentSpec struct { - DisplayName string `yaml:"displayName"` - Version string `yaml:"version"` - Context string `yaml:"context,omitempty"` - VHost string `yaml:"vhost,omitempty"` - Provider LLMProxyDeploymentProvider `yaml:"provider"` - Policies []api.LLMPolicy `yaml:"policies,omitempty"` + DisplayName string `yaml:"displayName"` + Version string `yaml:"version"` + Context string `yaml:"context,omitempty"` + VHost string `yaml:"vhost,omitempty"` + Provider LLMProxyDeploymentProvider `yaml:"provider"` + AdditionalProviders []LLMProxyDeploymentAdditionalProvider `yaml:"additionalProviders,omitempty"` + Policies []api.LLMPolicy `yaml:"policies,omitempty"` } type LLMProxyDeploymentProvider struct { ID string `yaml:"id"` Auth *api.UpstreamAuth `yaml:"auth,omitempty"` } + +type LLMProxyDeploymentAdditionalProvider struct { + ID string `yaml:"id"` + As string `yaml:"as,omitempty"` +} diff --git a/platform-api/src/internal/model/llm.go b/platform-api/src/internal/model/llm.go index 2332da5638..0b24f12e64 100644 --- a/platform-api/src/internal/model/llm.go +++ b/platform-api/src/internal/model/llm.go @@ -207,14 +207,24 @@ type LLMProxy struct { } type LLMProxyConfig struct { - Name string `json:"name,omitempty" db:"-"` - Version string `json:"version,omitempty" db:"-"` - Context *string `json:"context,omitempty" db:"-"` - Vhost *string `json:"vhost,omitempty" db:"-"` - Provider string `json:"provider,omitempty" db:"-"` - UpstreamAuth *UpstreamAuth `json:"upstreamAuth,omitempty" db:"-"` - Policies []LLMPolicy `json:"policies,omitempty" db:"-"` - Security *SecurityConfig `json:"security,omitempty" db:"-"` + Name string `json:"name,omitempty" db:"-"` + Version string `json:"version,omitempty" db:"-"` + Context *string `json:"context,omitempty" db:"-"` + Vhost *string `json:"vhost,omitempty" db:"-"` + Provider string `json:"provider,omitempty" db:"-"` + UpstreamAuth *UpstreamAuth `json:"upstreamAuth,omitempty" db:"-"` + AdditionalProviders []LLMProxyAdditionalProvider `json:"additionalProviders,omitempty" db:"-"` + Policies []LLMPolicy `json:"policies,omitempty" db:"-"` + Security *SecurityConfig `json:"security,omitempty" db:"-"` +} + +// LLMProxyAdditionalProvider is an additional LLM provider attached to a proxy +// as a selectable upstream. Policies route to it by the `As` name (which +// defaults to `ID` when empty). Provider upstream auth is taken from the +// referenced LlmProvider's own configuration. +type LLMProxyAdditionalProvider struct { + ID string `json:"id" db:"-"` + As string `json:"as,omitempty" db:"-"` } type SecurityConfig struct { diff --git a/platform-api/src/internal/service/llm.go b/platform-api/src/internal/service/llm.go index 1c112626e8..2ed710213b 100644 --- a/platform-api/src/internal/service/llm.go +++ b/platform-api/src/internal/service/llm.go @@ -630,12 +630,13 @@ func (s *LLMProxyService) Create(orgUUID, createdBy string, req *api.LLMProxy) ( OpenAPISpec: utils.ValueOrEmpty(req.Openapi), Status: llmStatusPending, Configuration: model.LLMProxyConfig{ - Context: &contextValue, - Vhost: req.Vhost, - Provider: req.Provider.Id, - UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), - Policies: mapPoliciesAPIToModel(req.Policies), - Security: mapSecurityAPIToModel(req.Security), + Context: &contextValue, + Vhost: req.Vhost, + Provider: req.Provider.Id, + UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), + AdditionalProviders: mapAdditionalProvidersAPIToModel(req.AdditionalProviders), + Policies: mapPoliciesAPIToModel(req.Policies), + Security: mapSecurityAPIToModel(req.Security), }, } @@ -842,12 +843,13 @@ func (s *LLMProxyService) Update(orgUUID, handle string, req *api.LLMProxy) (*ap OpenAPISpec: utils.ValueOrEmpty(req.Openapi), Status: llmStatusPending, Configuration: model.LLMProxyConfig{ - Context: &contextValue, - Vhost: req.Vhost, - Provider: req.Provider.Id, - UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), - Policies: mapPoliciesAPIToModel(req.Policies), - Security: mapSecurityAPIToModel(req.Security), + Context: &contextValue, + Vhost: req.Vhost, + Provider: req.Provider.Id, + UpstreamAuth: mapUpstreamAuthAPIToModel(req.Provider.Auth), + AdditionalProviders: mapAdditionalProvidersAPIToModel(req.AdditionalProviders), + Policies: mapPoliciesAPIToModel(req.Policies), + Security: mapSecurityAPIToModel(req.Security), }, } @@ -1048,6 +1050,36 @@ func mapUpstreamAuthAPIToModel(in *api.UpstreamAuth) *model.UpstreamAuth { } } +func mapAdditionalProvidersAPIToModel(in *[]api.LLMProxyAdditionalProvider) []model.LLMProxyAdditionalProvider { + if in == nil || len(*in) == 0 { + return nil + } + out := make([]model.LLMProxyAdditionalProvider, 0, len(*in)) + for _, p := range *in { + out = append(out, model.LLMProxyAdditionalProvider{ + ID: p.Id, + As: utils.ValueOrEmpty(p.As), + }) + } + return out +} + +func mapAdditionalProvidersModelToAPI(in []model.LLMProxyAdditionalProvider) *[]api.LLMProxyAdditionalProvider { + if len(in) == 0 { + return nil + } + out := make([]api.LLMProxyAdditionalProvider, 0, len(in)) + for _, p := range in { + entry := api.LLMProxyAdditionalProvider{Id: p.ID} + if p.As != "" { + as := p.As + entry.As = &as + } + out = append(out, entry) + } + return &out +} + func normalizeUpstreamAuthType(authType string) string { normalized := strings.TrimSpace(authType) if normalized == "" { @@ -1857,6 +1889,9 @@ func mapProxyModelToAPI(m *model.LLMProxy) *api.LLMProxy { Value: nil, // Redact auth credential value } } + if extra := mapAdditionalProvidersModelToAPI(m.Configuration.AdditionalProviders); extra != nil { + out.AdditionalProviders = extra + } if len(m.Configuration.Policies) > 0 { policyList := make([]api.LLMPolicy, 0, len(m.Configuration.Policies)) for _, p := range m.Configuration.Policies { diff --git a/platform-api/src/internal/service/llm_deployment.go b/platform-api/src/internal/service/llm_deployment.go index 29cfa6523f..9c5edba7e2 100644 --- a/platform-api/src/internal/service/llm_deployment.go +++ b/platform-api/src/internal/service/llm_deployment.go @@ -1681,7 +1681,8 @@ func generateLLMProxyDeploymentYAML(proxy *model.LLMProxy) (string, error) { Provider: dto.LLMProxyDeploymentProvider{ ID: proxy.Configuration.Provider, }, - Policies: policies, + AdditionalProviders: mapAdditionalProvidersModelToDeployment(proxy.Configuration.AdditionalProviders), + Policies: policies, }, } @@ -1719,6 +1720,20 @@ func mapModelUpstreamAuthToAPI(auth *model.UpstreamAuth) *api.UpstreamAuth { return mapModelAuthToAPI(auth) } +func mapAdditionalProvidersModelToDeployment(in []model.LLMProxyAdditionalProvider) []dto.LLMProxyDeploymentAdditionalProvider { + if len(in) == 0 { + return nil + } + out := make([]dto.LLMProxyDeploymentAdditionalProvider, 0, len(in)) + for _, p := range in { + out = append(out, dto.LLMProxyDeploymentAdditionalProvider{ + ID: p.ID, + As: p.As, + }) + } + return out +} + // orderLLMPolicies ensures llm-cost-based-ratelimit always precedes llm-cost in the policy list. // All other policies retain their relative positions. func orderLLMPolicies(policies []api.LLMPolicy) []api.LLMPolicy { diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index f49addf529..d7db41261f 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -10266,6 +10266,15 @@ components: example: "api.openai" provider: $ref: '#/components/schemas/LLMProxyProvider' + additionalProviders: + type: array + description: > + Optional list of additional LLM providers attached to this proxy as + selectable upstreams. Policies route requests to any of these by + setting the upstream name. The primary `provider` field above + remains the default upstream and the FK target. + items: + $ref: '#/components/schemas/LLMProxyAdditionalProvider' openapi: type: string description: OpenAPI specification (JSON or YAML) for the proxy endpoint @@ -10370,6 +10379,28 @@ components: auth: $ref: '#/components/schemas/UpstreamAuth' + LLMProxyAdditionalProvider: + type: object + required: + - id + description: > + Additional LLM provider attached to this proxy as a selectable upstream. + Policies route to it by referring to the `as` name (defaults to `id`). + properties: + id: + type: string + description: Unique id of a deployed llm provider + example: anthropic-provider + as: + type: string + description: > + Logical upstream name used by policies to select this provider. Must + be unique within the proxy. Defaults to `id` when omitted. + pattern: '^[a-zA-Z0-9\-_]+$' + minLength: 1 + maxLength: 100 + example: anthropic-upstream + CreateLLMProviderAPIKeyRequest: type: object properties: From 36db0520a011091684bee70289ac6be41da3941f Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Mon, 22 Jun 2026 13:52:34 +0530 Subject: [PATCH 04/12] feat(policy): add openai-to-gemini translation policy with request and response transformations --- .../default-policies/openai-to-gemini.yaml | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 event-gateway/default-policies/openai-to-gemini.yaml diff --git a/event-gateway/default-policies/openai-to-gemini.yaml b/event-gateway/default-policies/openai-to-gemini.yaml new file mode 100644 index 0000000000..48fe4b6fb9 --- /dev/null +++ b/event-gateway/default-policies/openai-to-gemini.yaml @@ -0,0 +1,86 @@ +name: openai-to-gemini +version: v1.0.0 +description: | + Translates an incoming OpenAI Chat Completions request into the Gemini + generateContent API format and forwards it to a Gemini-compatible + upstream. + + Request transformations: + - system/developer messages are lifted into the top-level + "systemInstruction" field. + - messages[] is converted to Gemini contents[] with role/parts blocks; + assistant tool_calls become functionCall parts and tool-role messages + become functionResponse parts. + - tools[] are converted to functionDeclarations and tool_choice to a + functionCallingConfig. + - sampling fields (temperature, top_p, max_tokens, stop, etc.) are mapped + into generationConfig; image_url parts become inlineData/fileData. + - The request path is rewritten to + /{apiVersion}/models/{model}:generateContent. + + Response transformations: + - The Gemini generateContent response body is rewritten into the OpenAI + ChatCompletion shape (id, object, created, model, choices, usage). + - text parts become choice.message.content; functionCall parts become + choice.message.tool_calls; finishReason is mapped to finish_reason and + usageMetadata token counts are carried in usage. + - Streaming (Server-Sent Events) responses are passed through untouched + in this policy version; clients that need true streaming should request + non-streaming (omit "stream": true) until a streaming variant ships. + + Execution modes: + + 1. Routed (multi-provider proxies): set the "provider" parameter to the + provider id this translator is bound to. The policy only runs when an + upstream setter (e.g. openai-header-router) publishes the same id into + SharedContext.Metadata["selected_provider"]. Non-matching requests + pass through untouched. + + 2. Unconditional (single-provider proxies): omit "provider". The + translator runs on every request. + + The upstream host and Gemini API key must be configured on the upstream + LlmProvider — this policy does not inject credentials. + +parameters: + type: object + additionalProperties: false + properties: + provider: + type: string + x-wso2-policy-advanced-param: false + description: | + Provider id this translator is bound to. When set, the policy runs + only if SharedContext.Metadata["selected_provider"] equals this + value. Omit for single-provider proxies. + minLength: 1 + model: + type: string + x-wso2-policy-advanced-param: false + description: | + Gemini model name used in the translated request (for example + gemini-2.5-pro). Overrides the OpenAI "model" field and is used in + the rewritten path. + minLength: 1 + upstreamName: + type: string + x-wso2-policy-advanced-param: false + description: | + Name of the upstream cluster declared in the LlmProxy + additionalProviders list. When omitted, routing is left to whatever + the route's default upstream resolves to. + minLength: 1 + apiVersion: + type: string + x-wso2-policy-advanced-param: true + description: | + Gemini API version segment used in the rewritten path + (/{apiVersion}/models/{model}:generateContent). + default: "v1beta" + minLength: 1 + required: + - model + +systemParameters: + type: object + properties: {} \ No newline at end of file From f871799fc5501e83e0a2d81b83c2357c7d3e8f91 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Mon, 22 Jun 2026 14:58:30 +0530 Subject: [PATCH 05/12] added build golang build file --- .../pkg/api/management/generated.go | 549 +++++++++--------- 1 file changed, 277 insertions(+), 272 deletions(-) diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index 2eb32544e0..fc5adeac85 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -5207,278 +5207,283 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "h8xIt8//JwAA//9q4TK55lQCAA==", + "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", + "9UNmpNvn/xMAAP//sKJAsX9ZAgA=", } // GetSwagger returns the content of the embedded swagger specification file From 7342ca6d114fbaf33d23c79ec76f7a49353cfd4f Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Tue, 23 Jun 2026 10:09:47 +0530 Subject: [PATCH 06/12] fix(examples): align api-key-auth paths with accessControl exceptions in provider examples All allowed paths in accessControl.exceptions now have matching api-key-auth entries so loopback callers must present an API key for every exposed endpoint, not just chat/completions. --- gateway/examples/azure-openai-provider.yaml | 15 +++++++++++++++ gateway/examples/mistral-provider.yaml | 10 ++++++++++ gateway/examples/openai-provider.yaml | 20 ++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/gateway/examples/azure-openai-provider.yaml b/gateway/examples/azure-openai-provider.yaml index e1345ac3c4..54f227392f 100644 --- a/gateway/examples/azure-openai-provider.yaml +++ b/gateway/examples/azure-openai-provider.yaml @@ -37,6 +37,21 @@ spec: params: key: X-API-Key in: header + - path: /openai/deployments/{deployment}/completions + methods: [POST] + params: + key: X-API-Key + in: header + - path: /openai/deployments/{deployment}/embeddings + methods: [POST] + params: + key: X-API-Key + in: header + - path: /openai/models + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: diff --git a/gateway/examples/mistral-provider.yaml b/gateway/examples/mistral-provider.yaml index 67ec1c2654..18a96bebdd 100644 --- a/gateway/examples/mistral-provider.yaml +++ b/gateway/examples/mistral-provider.yaml @@ -35,6 +35,16 @@ spec: params: key: X-API-Key in: header + - path: /v1/embeddings + methods: [POST] + params: + key: X-API-Key + in: header + - path: /v1/models + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: diff --git a/gateway/examples/openai-provider.yaml b/gateway/examples/openai-provider.yaml index b206cff3f2..483a5a3c42 100644 --- a/gateway/examples/openai-provider.yaml +++ b/gateway/examples/openai-provider.yaml @@ -35,6 +35,26 @@ spec: params: key: X-API-Key in: header + - path: /completions + methods: [POST] + params: + key: X-API-Key + in: header + - path: /embeddings + methods: [POST] + params: + key: X-API-Key + in: header + - path: /models + methods: [GET] + params: + key: X-API-Key + in: header + - path: /models/{modelId} + methods: [GET] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: From 601bc8482a9b12c84001651a4388602a044a40d6 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Tue, 23 Jun 2026 10:41:06 +0530 Subject: [PATCH 07/12] fix(examples): add missing loopback auth and align api-key-auth with access control exceptions - azure-openai-proxy: add provider.auth so the proxy can authenticate against the provider's api-key-auth policy on the loopback hop - gemini-provider: add streamGenerateContent to api-key-auth paths to match the accessControl exceptions list --- gateway/examples/azure-openai-proxy.yaml | 4 ++++ gateway/examples/gemini-provider.yaml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/gateway/examples/azure-openai-proxy.yaml b/gateway/examples/azure-openai-proxy.yaml index 0466a62e72..96199518ab 100644 --- a/gateway/examples/azure-openai-proxy.yaml +++ b/gateway/examples/azure-openai-proxy.yaml @@ -26,6 +26,10 @@ spec: context: /azure-openai provider: id: azure-openai-provider + auth: + type: api-key + header: X-API-Key + value: azure_openai_provider_loopback_key_1234567890abcdef policies: - name: openai-to-azure-openai version: v1 diff --git a/gateway/examples/gemini-provider.yaml b/gateway/examples/gemini-provider.yaml index 833bc962e5..8abcaccd05 100644 --- a/gateway/examples/gemini-provider.yaml +++ b/gateway/examples/gemini-provider.yaml @@ -32,6 +32,11 @@ spec: params: key: X-API-Key in: header + - path: /v1beta/models/{model}:streamGenerateContent + methods: [POST] + params: + key: X-API-Key + in: header accessControl: mode: deny_all exceptions: From a8ca55291b325359fb87498e7ea84d05e0e54e1e Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Tue, 23 Jun 2026 11:11:55 +0530 Subject: [PATCH 08/12] fix(examples): fix azure-openai-provider url placeholder and loopback auth, align gemini api-key-auth paths - azure-openai-provider: replace null url comment with a valid placeholder - azure-openai-proxy: add missing provider.auth for loopback authentication - gemini-provider: add streamGenerateContent to api-key-auth to match accessControl exceptions --- gateway/examples/azure-openai-provider.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/examples/azure-openai-provider.yaml b/gateway/examples/azure-openai-provider.yaml index 54f227392f..c1e5e998d3 100644 --- a/gateway/examples/azure-openai-provider.yaml +++ b/gateway/examples/azure-openai-provider.yaml @@ -23,7 +23,7 @@ spec: version: v1.0 template: azure-openai upstream: - url: #endpoint url + url: https://REPLACE_WITH_AZURE_OPENAI_ENDPOINT auth: type: api-key header: api-key From 918f345975ac295eb514cac1c36660427c07ca68 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Tue, 23 Jun 2026 11:16:35 +0530 Subject: [PATCH 09/12] fix(examples): replace dev placeholder with Bearer-formatted API key in mistral-provider --- gateway/examples/mistral-provider.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/examples/mistral-provider.yaml b/gateway/examples/mistral-provider.yaml index 18a96bebdd..d3c5776aec 100644 --- a/gateway/examples/mistral-provider.yaml +++ b/gateway/examples/mistral-provider.yaml @@ -25,7 +25,7 @@ spec: auth: type: api-key header: Authorization - value: $dxxxd + value: Bearer REPLACE_WITH_MISTRAL_API_KEY policies: - name: api-key-auth version: v1 From 0698cbcfc8d589aa8d1d88038c5654eb7fd819b7 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Thu, 25 Jun 2026 16:05:12 +0530 Subject: [PATCH 10/12] Multi provider proxy gemini addition --- .../examples/openai-multi-provider-proxy.yaml | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/gateway/examples/openai-multi-provider-proxy.yaml b/gateway/examples/openai-multi-provider-proxy.yaml index a8ba025970..ffdc6e0382 100644 --- a/gateway/examples/openai-multi-provider-proxy.yaml +++ b/gateway/examples/openai-multi-provider-proxy.yaml @@ -36,6 +36,11 @@ spec: type: api-key header: X-API-Key value: anthropic_provider_loopback_key_1234567890abcdef + transformer: + type: openai-to-anthropic + version: v1 + params: + model: claude-sonnet-4-5-20250929 - id: azure-openai-provider auth: type: api-key @@ -47,11 +52,11 @@ spec: header: X-API-Key value: mistral_provider_loopback_key_1234567890abcdef #mistral_provider_loopback_key_1234567890abcdef - # - id: gemini-provider - # auth: - # type: api-key - # header: X-API-Key - # value: gemini_provider_loopback_key_1234567890abcdef + - id: gemini-provider + auth: + type: api-key + header: X-API-Key + value: gemini_provider_loopback_key_1234567890abcdef policies: - name: api-key-auth @@ -79,8 +84,8 @@ spec: provider: azure-openai-provider - headerValue: mistral provider: mistral-provider - # - headerValue: gemini - # provider: gemini-provider + - headerValue: gemini + provider: gemini-provider # - name: token-based-ratelimit # version: v1 @@ -95,6 +100,7 @@ spec: # backend: memory - name: openai-to-anthropic + executionCondition: metadata["selected_provider"] == "anthropic-provider" version: v1 paths: - path: /chat/completions @@ -122,15 +128,15 @@ spec: model: mistral-large-latest id: mistral-provider - # - name: openai-to-gemini - # version: v1 - # paths: - # - path: /chat/completions - # methods: [POST] - # params: - # model: gemini-2.5-flash - # id: gemini-provider - # apiVersion: v1beta + - name: openai-to-gemini + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + model: gemini-2.5-flash + id: gemini-provider + apiVersion: v1beta --- # Client API key for this proxy. The api-key-auth policy above validates From 97e231e3f5b0b66d915df41616968afb61ff1d21 Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Thu, 2 Jul 2026 11:46:46 +0530 Subject: [PATCH 11/12] feat(gateway): move LLM proxy translator config inline onto additionalProviders Declares each provider's translator as additionalProviders[].transformer instead of a standalone top-level policy, so provider auth and its translator live together and the provider id no longer needs to be duplicated in the translator's params. Adds validation for missing type/version and IT coverage for routing, validation, and persistence. --- .../examples/openai-multi-provider-proxy.yaml | 59 +- .../api/management-openapi.yaml | 31 + .../pkg/api/management/generated.go | 573 +++++++++--------- .../pkg/config/llm_validator.go | 21 + .../pkg/utils/llm_transformer.go | 70 ++- .../pkg/utils/llm_transformer_test.go | 96 +++ gateway/it/features/llm-proxies.feature | 345 +++++++++++ 7 files changed, 867 insertions(+), 328 deletions(-) diff --git a/gateway/examples/openai-multi-provider-proxy.yaml b/gateway/examples/openai-multi-provider-proxy.yaml index ffdc6e0382..c501a76bd0 100644 --- a/gateway/examples/openai-multi-provider-proxy.yaml +++ b/gateway/examples/openai-multi-provider-proxy.yaml @@ -46,18 +46,33 @@ spec: type: api-key header: X-API-Key value: azure_openai_provider_loopback_key_1234567890abcdef + transformer: + type: openai-to-azure-openai + version: v1 + params: + model: gpt-4o + apiVersion: "2024-02-15-preview" - id: mistral-provider auth: type: api-key header: X-API-Key value: mistral_provider_loopback_key_1234567890abcdef - #mistral_provider_loopback_key_1234567890abcdef + transformer: + type: openai-to-mistral + version: v1 + params: + model: mistral-large-latest - id: gemini-provider auth: type: api-key header: X-API-Key value: gemini_provider_loopback_key_1234567890abcdef - + transformer: + type: openai-to-gemini + version: v1 + params: + model: gemini-2.5-flash + apiVersion: v1beta policies: - name: api-key-auth version: v1 @@ -98,46 +113,6 @@ spec: # duration: "1m" # algorithm: fixed-window # backend: memory - - - name: openai-to-anthropic - executionCondition: metadata["selected_provider"] == "anthropic-provider" - version: v1 - paths: - - path: /chat/completions - methods: [POST] - params: - model: claude-sonnet-4-5-20250929 - id: anthropic-provider - - - name: openai-to-azure-openai - version: v1 - paths: - - path: /chat/completions - methods: [POST] - params: - apiVersion: "2024-02-15-preview" - model: gpt-4o - id: azure-openai-provider - - - name: openai-to-mistral - version: v1 - paths: - - path: /chat/completions - methods: [POST] - params: - model: mistral-large-latest - id: mistral-provider - - - name: openai-to-gemini - version: v1 - paths: - - path: /chat/completions - methods: [POST] - params: - model: gemini-2.5-flash - id: gemini-provider - apiVersion: v1beta - --- # Client API key for this proxy. The api-key-auth policy above validates # the X-API-Key header against the key the gateway issues for this resource. diff --git a/gateway/gateway-controller/api/management-openapi.yaml b/gateway/gateway-controller/api/management-openapi.yaml index 7dd022450a..00df4e30f1 100644 --- a/gateway/gateway-controller/api/management-openapi.yaml +++ b/gateway/gateway-controller/api/management-openapi.yaml @@ -5851,6 +5851,37 @@ components: example: anthropic-upstream auth: $ref: '#/components/schemas/LLMUpstreamAuth' + transformer: + $ref: '#/components/schemas/LLMProxyTransformer' + + LLMProxyTransformer: + type: object + required: + - type + - version + description: > + Request/response translator applied when this provider is the selected + upstream. The proxy injects the translator as a conditional policy whose + execution condition matches this provider, so it runs only when the + provider is selected. The provider's `as` name (defaults to `id`) is + passed to the translator as its target upstream. + properties: + type: + type: string + description: Translator policy name (for example openai-to-anthropic). + minLength: 1 + example: openai-to-anthropic + version: + type: string + description: > + Major-only translator policy version (for example v1). The Gateway + Controller resolves it to the installed full version. + minLength: 1 + example: v1 + params: + type: object + description: Translator-specific parameters (for example model, apiVersion). + additionalProperties: true LLMAccessControl: type: object diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index fc5adeac85..efbcf3ce26 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -925,6 +925,9 @@ type LLMProxyAdditionalProvider struct { // Id Unique id of a deployed llm provider Id string `json:"id" yaml:"id"` + + // Transformer Request/response translator applied when this provider is the selected upstream. The proxy injects the translator as a conditional policy whose execution condition matches this provider, so it runs only when the provider is selected. The provider's `as` name (defaults to `id`) is passed to the translator as its target upstream. + Transformer *LLMProxyTransformer `json:"transformer,omitempty" yaml:"transformer,omitempty"` } // LLMProxyConfigData defines model for LLMProxyConfigData. @@ -1000,6 +1003,18 @@ type LLMProxyProvider struct { Id string `json:"id" yaml:"id"` } +// LLMProxyTransformer Request/response translator applied when this provider is the selected upstream. The proxy injects the translator as a conditional policy whose execution condition matches this provider, so it runs only when the provider is selected. The provider's `as` name (defaults to `id`) is passed to the translator as its target upstream. +type LLMProxyTransformer struct { + // Params Translator-specific parameters (for example model, apiVersion). + Params *map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"` + + // Type Translator policy name (for example openai-to-anthropic). + Type string `json:"type" yaml:"type"` + + // Version Major-only translator policy version (for example v1). The Gateway Controller resolves it to the installed full version. + Version string `json:"version" yaml:"version"` +} + // LLMUpstreamAuth defines model for LLMUpstreamAuth. type LLMUpstreamAuth struct { Header *string `json:"header,omitempty" yaml:"header,omitempty"` @@ -5207,283 +5222,287 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "9UNmpNvn/xMAAP//sKJAsX9ZAgA=", + "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", + "k9+PZ05dAgA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/gateway/gateway-controller/pkg/config/llm_validator.go b/gateway/gateway-controller/pkg/config/llm_validator.go index cb44132b63..107481c7c8 100644 --- a/gateway/gateway-controller/pkg/config/llm_validator.go +++ b/gateway/gateway-controller/pkg/config/llm_validator.go @@ -608,12 +608,33 @@ func (v *LLMValidator) validateProxyData(spec *api.LLMProxyConfigData) []Validat if provider.Auth != nil { errors = append(errors, v.validateLLMUpstreamAuth(fieldPrefix+".auth", provider.Auth)...) } + + if provider.Transformer != nil { + errors = append(errors, v.validateLLMProxyTransformer(fieldPrefix+".transformer", provider.Transformer)...) + } } } return errors } +func (v *LLMValidator) validateLLMProxyTransformer(fieldPrefix string, transformer *api.LLMProxyTransformer) []ValidationError { + var errors []ValidationError + if transformer.Type == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".type", + Message: "Transformer type is required", + }) + } + if transformer.Version == "" { + errors = append(errors, ValidationError{ + Field: fieldPrefix + ".version", + Message: "Transformer version is required", + }) + } + return errors +} + func (v *LLMValidator) validateLLMUpstreamAuth(fieldPrefix string, auth *api.LLMUpstreamAuth) []ValidationError { var errors []ValidationError if auth.Type == "" { diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer.go b/gateway/gateway-controller/pkg/utils/llm_transformer.go index 701114746c..b9de661cd0 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer.go @@ -226,7 +226,10 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration } // Step 3.5: Apply proxy-level provider auth for proxy->provider loopback upstream + // and inline translators declared per additional provider. Both are attached as + // conditional policies so they run only when their provider is selected. var upstreamAuthPolicies []api.Policy + var transformerPolicies []api.Policy if proxy.Spec.Provider.Auth != nil { pol, err := t.proxyUpstreamAuthPolicy(proxy.Spec.Provider.Auth, "provider.auth") if err != nil { @@ -238,20 +241,28 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration } if proxy.Spec.AdditionalProviders != nil { for _, ap := range *proxy.Spec.AdditionalProviders { - if ap.Auth == nil { - continue - } name := ap.Id if ap.As != nil && *ap.As != "" { name = *ap.As } - pol, err := t.proxyUpstreamAuthPolicy(ap.Auth, fmt.Sprintf("additionalProviders[%s].auth", name)) - if err != nil { - return nil, err + + if ap.Auth != nil { + pol, err := t.proxyUpstreamAuthPolicy(ap.Auth, fmt.Sprintf("additionalProviders[%s].auth", name)) + if err != nil { + return nil, err + } + condition := selectedProviderExecutionCondition(name, false) + pol.ExecutionCondition = &condition + upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) + } + + if ap.Transformer != nil { + pol, err := t.proxyTransformerPolicy(ap.Transformer, name, fmt.Sprintf("additionalProviders[%s].transformer", name)) + if err != nil { + return nil, err + } + transformerPolicies = append(transformerPolicies, *pol) } - condition := selectedProviderExecutionCondition(name, false) - pol.ExecutionCondition = &condition - upstreamAuthPolicies = append(upstreamAuthPolicies, *pol) } } @@ -324,6 +335,15 @@ func (t *LLMProviderTransformer) transformProxy(proxy *api.LLMProxyConfiguration ops = append(ops, *op) } ops = sortOperationsBySpecificity(ops) + // Translators must run before upstream auth so the request is rewritten into + // the selected provider's shape before the upstream key is added. + if len(transformerPolicies) > 0 { + for i := range ops { + for _, transformerPolicy := range transformerPolicies { + appendOperationPolicy(&ops[i], transformerPolicy) + } + } + } if len(upstreamAuthPolicies) > 0 { for i := range ops { for _, upstreamAuthPolicy := range upstreamAuthPolicies { @@ -693,6 +713,38 @@ func (t *LLMProviderTransformer) proxyUpstreamAuthPolicy(auth *api.LLMUpstreamAu } } +// proxyTransformerPolicy builds a translator policy for an additional provider's +// inline transformer. The provider's upstream name is passed to the translator as +// its "id" param so it targets the correct upstream, and gates execution so the +// translator runs only when this provider is the selected upstream. +func (t *LLMProviderTransformer) proxyTransformerPolicy(transformer *api.LLMProxyTransformer, name, field string) (*api.Policy, error) { + if transformer == nil { + return nil, nil + } + if transformer.Type == "" { + return nil, fmt.Errorf("%s.type is required", field) + } + if transformer.Version == "" { + return nil, fmt.Errorf("%s.version is required", field) + } + + params := map[string]interface{}{} + if transformer.Params != nil { + for k, v := range *transformer.Params { + params[k] = v + } + } + params["id"] = name + + condition := selectedProviderExecutionCondition(name, false) + return &api.Policy{ + Name: transformer.Type, + Version: transformer.Version, + Params: ¶ms, + ExecutionCondition: &condition, + }, nil +} + func selectedProviderExecutionCondition(providerName string, includeDefault bool) string { selectedExpr := fmt.Sprintf("request.Metadata['selected_provider'] == '%s'", providerName) if includeDefault { diff --git a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go index 5bc2aaad8a..c78894b069 100644 --- a/gateway/gateway-controller/pkg/utils/llm_transformer_test.go +++ b/gateway/gateway-controller/pkg/utils/llm_transformer_test.go @@ -313,6 +313,102 @@ func TestLLMProviderTransformer_TransformProxy_AdditionalProviderAuthIsCondition assert.Equal(t, "anthropic-loopback", firstRequestHeaderValue(t, authPolicies[1].Params)) } +func TestLLMProviderTransformer_TransformProxy_AdditionalProviderTransformerIsConditional(t *testing.T) { + store := storage.NewConfigStore() + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + db := newTestSQLiteStorage(t, logger) + + template := &models.StoredLLMProviderTemplate{ + UUID: "0000-db-template-id-0000-000000000003", + Configuration: api.LLMProviderTemplate{ + ApiVersion: api.LLMProviderTemplateApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderTemplateKindLlmProviderTemplate, + Metadata: api.Metadata{Name: "openai"}, + Spec: api.LLMProviderTemplateData{DisplayName: "openai"}, + }, + } + require.NoError(t, db.SaveLLMProviderTemplate(template)) + + saveProvider := func(name, context string) { + providerSourceConfig := api.LLMProviderConfiguration{ + ApiVersion: api.LLMProviderConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProviderConfigurationKindLlmProvider, + Metadata: api.Metadata{Name: name}, + Spec: api.LLMProviderConfigData{ + DisplayName: name, + Version: "v1.0", + Context: stringPtr(context), + Template: "openai", + Upstream: api.LLMProviderConfigData_Upstream{Url: stringPtr("https://example.com")}, + AccessControl: api.LLMAccessControl{Mode: api.AllowAll}, + }, + } + require.NoError(t, db.SaveConfig(&models.StoredConfig{ + UUID: name + "-uuid", + Kind: string(api.LLMProviderConfigurationKindLlmProvider), + Handle: name, + DisplayName: name, + Version: "v1.0", + SourceConfiguration: providerSourceConfig, + DesiredState: models.StateDeployed, + })) + } + saveProvider("openai-provider", "/openai-provider") + saveProvider("anthropic-provider", "/anthropic-provider") + + transformer := NewLLMProviderTransformer(store, db, &config.RouterConfig{ListenerPort: 8080}, newTestPolicyVersionResolver()) + + proxy := &api.LLMProxyConfiguration{ + ApiVersion: api.LLMProxyConfigurationApiVersionGatewayApiPlatformWso2Comv1alpha1, + Kind: api.LLMProxyConfigurationKindLlmProxy, + Metadata: api.Metadata{Name: "openai-multi"}, + Spec: api.LLMProxyConfigData{ + DisplayName: "openai-multi", + Version: "v1.0", + Provider: api.LLMProxyProvider{Id: "openai-provider"}, + AdditionalProviders: &[]api.LLMProxyAdditionalProvider{{ + Id: "anthropic-provider", + Transformer: &api.LLMProxyTransformer{ + Type: "openai-to-anthropic", + Version: "v1", + Params: &map[string]interface{}{ + "model": "claude-sonnet-4-5-20250929", + }, + }, + }}, + }, + } + + result, err := transformer.Transform(proxy, &api.RestAPI{}) + require.NoError(t, err) + + // The translator is attached conditionally to every operation, so locate it + // wherever it lands rather than assuming a specific route. + var transformerPolicy *api.Policy + for i := range result.Spec.Operations { + op := result.Spec.Operations[i] + if op.Policies == nil { + continue + } + for j := range *op.Policies { + if (*op.Policies)[j].Name == "openai-to-anthropic" { + transformerPolicy = &(*op.Policies)[j] + break + } + } + if transformerPolicy != nil { + break + } + } + require.NotNil(t, transformerPolicy) + assert.Equal(t, "v1", transformerPolicy.Version) + require.NotNil(t, transformerPolicy.ExecutionCondition) + assert.Contains(t, *transformerPolicy.ExecutionCondition, "anthropic-provider") + require.NotNil(t, transformerPolicy.Params) + assert.Equal(t, "anthropic-provider", (*transformerPolicy.Params)["id"]) + assert.Equal(t, "claude-sonnet-4-5-20250929", (*transformerPolicy.Params)["model"]) +} + func TestGetUpstreamAuthApikeyPolicyParams_Extended(t *testing.T) { t.Run("Valid parameters", func(t *testing.T) { params, err := GetUpstreamAuthApikeyPolicyParams("Authorization", "Bearer token123") diff --git a/gateway/it/features/llm-proxies.feature b/gateway/it/features/llm-proxies.feature index 7348d7d198..73389ba919 100644 --- a/gateway/it/features/llm-proxies.feature +++ b/gateway/it/features/llm-proxies.feature @@ -595,3 +595,348 @@ Feature: LLM Proxy Management Operations Then the response should be successful When I delete the LLM provider "multi-request-provider" Then the response status code should be 200 + + # ==================== MULTI-PROVIDER ROUTING WITH executionCondition ==================== + + Scenario: Multi-provider proxy routes to correct provider based on x-provider header + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: ec-primary-provider + spec: + displayName: EC Primary Provider + version: v1.0 + template: openai + context: /ec-primary + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: ec-secondary-provider + spec: + displayName: EC Secondary Provider + version: v1.0 + template: openai + context: /ec-secondary + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProxy + metadata: + name: ec-multi-provider-proxy + spec: + displayName: EC Multi Provider Proxy + version: v1.0 + context: /ec-multi-proxy + provider: + id: ec-primary-provider + additionalProviders: + - id: ec-secondary-provider + policies: + - name: openai-header-router + version: v1 + paths: + - path: /chat/completions + methods: [POST] + params: + headerName: x-provider + defaultProvider: ec-primary-provider + mappings: + - headerValue: primary + provider: ec-primary-provider + - headerValue: secondary + provider: ec-secondary-provider + """ + Then the response status should be 201 + And I wait for 3 seconds + + # No x-provider header — falls back to the defaultProvider (ec-primary-provider) + When I set header "Content-Type" to "application/json" + And I send a POST request to "http://localhost:8080/ec-multi-proxy/chat/completions" with body: + """ + { + "model": "gpt-4", + "messages": [{"role": "user", "content": "hello"}] + } + """ + Then the response status code should be 200 + And the response should be valid JSON + And the JSON response field "object" should be "chat.completion" + + # x-provider: secondary routes to ec-secondary-provider + When I set header "x-provider" to "secondary" + And I set header "Content-Type" to "application/json" + And I send a POST request to "http://localhost:8080/ec-multi-proxy/chat/completions" with body: + """ + { + "model": "gpt-4", + "messages": [{"role": "user", "content": "hello"}] + } + """ + Then the response status code should be 200 + And the response should be valid JSON + And the JSON response field "object" should be "chat.completion" + + # x-provider: primary explicitly selects ec-primary-provider + When I set header "x-provider" to "primary" + And I set header "Content-Type" to "application/json" + And I send a POST request to "http://localhost:8080/ec-multi-proxy/chat/completions" with body: + """ + { + "model": "gpt-4", + "messages": [{"role": "user", "content": "hello"}] + } + """ + Then the response status code should be 200 + And the response should be valid JSON + And the JSON response field "object" should be "chat.completion" + + # Cleanup + When I send a DELETE request to the "gateway-controller" service at "/llm-proxies/ec-multi-provider-proxy" + Then the response should be successful + When I delete the LLM provider "ec-primary-provider" + Then the response status code should be 200 + When I delete the LLM provider "ec-secondary-provider" + Then the response status code should be 200 + + # ==================== INLINE TRANSFORMER IN ADDITIONAL PROVIDERS ==================== + + Scenario: Create LLM proxy with additionalProviders transformer missing type returns validation error + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: transformer-val-primary + spec: + displayName: Transformer Validation Primary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: transformer-val-secondary + spec: + displayName: Transformer Validation Secondary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProxy + metadata: + name: transformer-missing-type-proxy + spec: + displayName: Transformer Missing Type Proxy + version: v1.0 + provider: + id: transformer-val-primary + additionalProviders: + - id: transformer-val-secondary + transformer: + version: v1.0.0 + params: + model: claude-sonnet-4-5-20250929 + """ + Then the response should be a client error + And the response should be valid JSON + # Cleanup + When I delete the LLM provider "transformer-val-primary" + Then the response status code should be 200 + When I delete the LLM provider "transformer-val-secondary" + Then the response status code should be 200 + + Scenario: Create LLM proxy with additionalProviders transformer missing version returns validation error + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: transformer-ver-primary + spec: + displayName: Transformer Version Primary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: transformer-ver-secondary + spec: + displayName: Transformer Version Secondary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProxy + metadata: + name: transformer-missing-ver-proxy + spec: + displayName: Transformer Missing Version Proxy + version: v1.0 + provider: + id: transformer-ver-primary + additionalProviders: + - id: transformer-ver-secondary + transformer: + type: openai-to-anthropic + params: + model: claude-sonnet-4-5-20250929 + """ + Then the response should be a client error + And the response should be valid JSON + # Cleanup + When I delete the LLM provider "transformer-ver-primary" + Then the response status code should be 200 + When I delete the LLM provider "transformer-ver-secondary" + Then the response status code should be 200 + + Scenario: Create LLM proxy with valid inline transformer in additionalProviders persists config + Given I authenticate using basic auth as "admin" + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: inline-transformer-primary + spec: + displayName: Inline Transformer Primary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I create this LLM provider: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProvider + metadata: + name: inline-transformer-secondary + spec: + displayName: Inline Transformer Secondary + version: v1.0 + template: openai + upstream: + url: http://mock-openapi:4010/openai/v1 + auth: + type: api-key + header: Authorization + value: Bearer sk-test-key + accessControl: + mode: allow_all + """ + Then the response status code should be 201 + When I deploy this LLM proxy configuration: + """ + apiVersion: gateway.api-platform.wso2.com/v1alpha1 + kind: LlmProxy + metadata: + name: inline-transformer-proxy + spec: + displayName: Inline Transformer Proxy + version: v1.0 + provider: + id: inline-transformer-primary + additionalProviders: + - id: inline-transformer-secondary + transformer: + type: openai-to-anthropic + version: v1.0.0 + params: + model: claude-sonnet-4-5-20250929 + """ + Then the response status should be 201 + And the response should be valid JSON + And the JSON response field "status" should be "success" + # Verify the transformer config is persisted + When I send a GET request to the "gateway-controller" service at "/llm-proxies/inline-transformer-proxy" + Then the response should be successful + And the response should be valid JSON + And the response body should contain "openai-to-anthropic" + And the response body should contain "inline-transformer-secondary" + # Cleanup + When I send a DELETE request to the "gateway-controller" service at "/llm-proxies/inline-transformer-proxy" + Then the response should be successful + When I delete the LLM provider "inline-transformer-primary" + Then the response status code should be 200 + When I delete the LLM provider "inline-transformer-secondary" + Then the response status code should be 200 From 1ceecefae6b8b77c4f782992a67e72f286787e6a Mon Sep 17 00:00:00 2001 From: Aakash Wijesekara Date: Mon, 6 Jul 2026 19:47:08 +0530 Subject: [PATCH 12/12] chore(deps): upgrade dependencies to meet governance constraints - oapi-codegen/runtime: v1.1.2 -> v1.2.0 (>=v1.2.0 required) - go-playground/validator: v10.29.0 -> v10.30.1 (>=v10.30.1 required) --- cli/src/go.mod | 4 ++-- common/go.mod | 2 +- gateway/gateway-controller/go.mod | 4 ++-- gateway/it/go.mod | 2 +- go.work.sum | 6 ++++-- platform-api/src/go.mod | 4 ++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cli/src/go.mod b/cli/src/go.mod index a0cd91ccba..d66331a5f9 100644 --- a/cli/src/go.mod +++ b/cli/src/go.mod @@ -24,7 +24,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.29.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect github.com/google/uuid v1.6.0 // indirect @@ -38,7 +38,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/runtime v1.1.2 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/common/go.mod b/common/go.mod index d863e58c9c..0829cc198f 100644 --- a/common/go.mod +++ b/common/go.mod @@ -25,7 +25,7 @@ require ( github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.29.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/gateway/gateway-controller/go.mod b/gateway/gateway-controller/go.mod index 3bfa669f38..ea32aad03e 100644 --- a/gateway/gateway-controller/go.mod +++ b/gateway/gateway-controller/go.mod @@ -17,7 +17,7 @@ require ( github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/v2 v2.3.2 github.com/mattn/go-sqlite3 v1.14.41 - github.com/oapi-codegen/runtime v1.1.2 + github.com/oapi-codegen/runtime v1.2.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/stretchr/testify v1.11.1 @@ -52,7 +52,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.29.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect diff --git a/gateway/it/go.mod b/gateway/it/go.mod index bafae2a336..b3c194749e 100644 --- a/gateway/it/go.mod +++ b/gateway/it/go.mod @@ -135,7 +135,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/oapi-codegen/runtime v1.1.2 // indirect + github.com/oapi-codegen/runtime v1.2.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect diff --git a/go.work.sum b/go.work.sum index 170c3c26c5..a2643fd0ff 100644 --- a/go.work.sum +++ b/go.work.sum @@ -2531,6 +2531,8 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= @@ -2955,8 +2957,6 @@ github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPucc github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= -github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4= -github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -3098,6 +3098,8 @@ github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= diff --git a/platform-api/src/go.mod b/platform-api/src/go.mod index 30b52e6a66..11d79b8de4 100644 --- a/platform-api/src/go.mod +++ b/platform-api/src/go.mod @@ -5,7 +5,7 @@ go 1.26.2 require ( github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 - github.com/go-playground/validator/v10 v10.29.0 + github.com/go-playground/validator/v10 v10.30.1 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 @@ -16,7 +16,7 @@ require ( github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/v2 v2.3.2 github.com/mattn/go-sqlite3 v1.14.41 - github.com/oapi-codegen/runtime v1.1.2 + github.com/oapi-codegen/runtime v1.2.0 github.com/pb33f/libopenapi v0.28.2 github.com/wso2/api-platform/common v0.0.0 golang.org/x/crypto v0.47.0