diff --git a/api/controllers/API_SCHEMA_GUIDE.md b/api/controllers/API_SCHEMA_GUIDE.md
index 30e6ddc0a8d..b9587b9f8f8 100644
--- a/api/controllers/API_SCHEMA_GUIDE.md
+++ b/api/controllers/API_SCHEMA_GUIDE.md
@@ -239,12 +239,12 @@ to the legacy area and avoid importing RESTX model objects from controllers.
## Verifying Swagger
-For schema and documentation changes, run focused tests and generate Swagger JSON:
+For schema and documentation changes, run focused tests and generate Swagger JSON from the repository root:
```bash
-uv run --project . pytest tests/unit_tests/controllers/common/test_schema.py
-uv run --project . pytest tests/unit_tests/commands/test_generate_swagger_specs.py tests/unit_tests/controllers/test_swagger.py
-uv run --project . dev/generate_swagger_specs.py --output-dir /tmp/dify-openapi-check
+uv run --project api pytest api/tests/unit_tests/controllers/common/test_schema.py
+uv run --project api pytest api/tests/unit_tests/commands/test_generate_swagger_specs.py api/tests/unit_tests/controllers/test_swagger.py
+uv run --project api python api/dev/generate_swagger_specs.py --output-dir /tmp/dify-openapi-check
```
Inspect affected endpoints with `jq`. Check that:
@@ -253,3 +253,17 @@ Inspect affected endpoints with `jq`. Check that:
- Request bodies appear only where the endpoint has a body.
- Responses reference the expected `*Response` schema.
- Response schemas use public serialized names, not internal validation aliases like `inputs_dict`.
+
+## Service API Documentation Handoff
+
+The `dify-docs` repository imports `service-openapi.json` for the `/v1` API. It keeps a pinned export and adds
+descriptions, examples, translations, and existing page URLs through documentation annotations. Fields, constraints,
+references, response statuses, and security come from this repository's export.
+
+Verify the public schema against request validation and response serialization, including intentional schema overrides
+and excluded fields. Fix an inaccurate contract here and regenerate it before updating the documentation snapshot.
+Record the full source commit SHA when handing off the export. For coordinated changes that are not committed yet,
+also provide the source patch used to produce it; replace that patch with a clean committed export after merging.
+
+The manual import, annotation review, build, and validation commands live in `tools/api-pipeline/README.md` in
+`dify-docs`. Regenerate this repository's OpenAPI Markdown and TypeScript/Zod contracts whenever their inputs change.
diff --git a/api/controllers/service_api/app/annotation.py b/api/controllers/service_api/app/annotation.py
index 0b54d133661..9b477e4e5e6 100644
--- a/api/controllers/service_api/app/annotation.py
+++ b/api/controllers/service_api/app/annotation.py
@@ -150,7 +150,6 @@ class AnnotationReplyActionStatusApi(Resource):
responses={
200: "Job status retrieved successfully",
401: "Unauthorized - invalid API token",
- 404: "Job not found",
}
)
@service_api_ns.response(
diff --git a/api/controllers/service_api/app/app.py b/api/controllers/service_api/app/app.py
index 8180111a4c1..a08752ba2eb 100644
--- a/api/controllers/service_api/app/app.py
+++ b/api/controllers/service_api/app/app.py
@@ -60,7 +60,6 @@ class AppParameterApi(Resource):
responses={
200: "Parameters retrieved successfully",
401: "Unauthorized - invalid API token",
- 404: "Application not found",
}
)
@service_api_ns.response(200, "Parameters retrieved successfully", service_api_ns.models[Parameters.__name__])
@@ -88,6 +87,7 @@ class AppMetaApi(Resource):
tags=["Applications"],
responses={
200: "Successfully retrieved application meta information.",
+ 400: "`app_unavailable` : App unavailable or misconfigured.",
},
)
@service_api_ns.doc("get_app_meta")
@@ -96,7 +96,6 @@ class AppMetaApi(Resource):
responses={
200: "Metadata retrieved successfully",
401: "Unauthorized - invalid API token",
- 404: "Application not found",
}
)
@service_api_ns.response(200, "Metadata retrieved successfully", service_api_ns.models[AppMetaResponse.__name__])
@@ -122,6 +121,7 @@ class AppInfoApi(Resource):
tags=["Applications"],
responses={
200: "Basic information of the application.",
+ 400: "`app_unavailable` : App unavailable or misconfigured.",
},
)
@service_api_ns.doc("get_app_info")
@@ -130,7 +130,6 @@ class AppInfoApi(Resource):
responses={
200: "Application info retrieved successfully",
401: "Unauthorized - invalid API token",
- 404: "Application not found",
}
)
@service_api_ns.response(
diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py
index 5365a1ab551..6bd0e2fe8a3 100644
--- a/api/controllers/service_api/app/completion.py
+++ b/api/controllers/service_api/app/completion.py
@@ -208,7 +208,6 @@ class CompletionApi(Resource):
200: "Completion created successfully",
400: "Bad request - invalid parameters",
401: "Unauthorized - invalid API token",
- 404: "Conversation not found",
500: "Internal server error",
}
)
diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py
index 939058ba271..b3308c76c46 100644
--- a/api/controllers/service_api/app/workflow.py
+++ b/api/controllers/service_api/app/workflow.py
@@ -315,7 +315,8 @@ class WorkflowRunApi(Resource):
),
429: (
"- `too_many_requests` : Too many concurrent requests for this app.\n"
- "- `rate_limit_error` : The upstream model provider rate limit was exceeded."
+ "- `rate_limit_error` : The upstream model provider rate limit or the Dify Cloud workflow execution "
+ "quota was exceeded."
),
500: "`internal_server_error` : Internal server error.",
},
@@ -329,7 +330,6 @@ class WorkflowRunApi(Resource):
200: "Workflow executed successfully",
400: "Bad request - invalid parameters or workflow issues",
401: "Unauthorized - invalid API token",
- 404: "Workflow not found",
429: "Rate limit exceeded",
500: "Internal server error",
}
@@ -430,7 +430,8 @@ class WorkflowRunByIdApi(Resource):
404: "`not_found` : Workflow not found.",
429: (
"- `too_many_requests` : Too many concurrent requests for this app.\n"
- "- `rate_limit_error` : The upstream model provider rate limit was exceeded."
+ "- `rate_limit_error` : The upstream model provider rate limit or the Dify Cloud workflow execution "
+ "quota was exceeded."
),
500: "`internal_server_error` : Internal server error.",
},
diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py
index 32035f539b1..13379b1e5cf 100644
--- a/api/controllers/service_api/dataset/dataset.py
+++ b/api/controllers/service_api/dataset/dataset.py
@@ -35,7 +35,8 @@ from controllers.service_api.wraps import (
from core.plugin.impl.model_runtime_factory import create_plugin_provider_manager
from core.rag.index_processor.constant.index_type import IndexTechniqueType
from fields.base import ResponseModel
-from fields.dataset_fields import DatasetDetailResponse, dataset_detail_response_source
+from fields.dataset_fields import DatasetDetailResponse as BaseDatasetDetailResponse
+from fields.dataset_fields import dataset_detail_response_source
from graphon.model_runtime.entities.model_entities import ModelType
from libs.helper import dump_response
from libs.login import current_user
@@ -88,6 +89,11 @@ PartialMemberList = Annotated[
]
+class DatasetDetailResponse(BaseDatasetDetailResponse):
+ # The Service API dump helpers exclude Console permission metadata.
+ permission_keys: list[str] = Field(default_factory=list, exclude=True)
+
+
_SERVICE_DATASET_DETAIL_EXCLUDE = {"permission_keys"}
_SERVICE_DATASET_LIST_EXCLUDE = {"data": {"__all__": _SERVICE_DATASET_DETAIL_EXCLUDE}}
@@ -738,18 +744,11 @@ class DatasetApi(DatasetApiResource):
@service_api_ns.doc(
summary="Delete Knowledge Base",
- description=(
- "Permanently delete a knowledge base and all its documents. The knowledge base must not be "
- "in use by any application."
- ),
+ description="Permanently delete a knowledge base and all its documents.",
tags=["Knowledge Bases"],
responses={
204: "Success.",
404: "`not_found` : Dataset not found.",
- 409: (
- "`dataset_in_use` : The knowledge base is being used by some apps. Please remove it from the "
- "apps before deleting."
- ),
},
)
@service_api_ns.doc("delete_dataset")
@@ -760,7 +759,6 @@ class DatasetApi(DatasetApiResource):
204: "Dataset deleted successfully",
401: "Unauthorized - invalid API token",
404: "Dataset not found",
- 409: "Conflict - dataset is in use",
}
)
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py
index a2288a99c43..37170840c93 100644
--- a/api/controllers/service_api/dataset/document.py
+++ b/api/controllers/service_api/dataset/document.py
@@ -211,7 +211,11 @@ def _non_null_property_schema(property_schema: object) -> dict[str, Any]:
]
if len(non_null_candidates) == 1:
return {
- **{key: value for key, value in property_schema.items() if key != "anyOf"},
+ **{
+ key: value
+ for key, value in property_schema.items()
+ if key != "anyOf" and not (key == "default" and value is None)
+ },
**deepcopy(non_null_candidates[0]),
}
@@ -246,7 +250,7 @@ class DocumentGetQuery(BaseModel):
default="all",
description=(
"`all` returns all fields including metadata. `only` returns only `id`, `doc_type`, and "
- "`doc_metadata`. `without` returns all fields except `doc_metadata`."
+ "`doc_metadata`. `without` returns all fields except `doc_type` and `doc_metadata`."
),
)
@@ -305,39 +309,53 @@ def _document_and_batch_response(document: Document, batch: str, *, session: Ses
)
-# Use SkipJsonSchema to support 3 metadata modes
+def _omit_schema_default(schema: dict[str, Any]) -> None:
+ """Keep omission placeholders out of the public non-null field contract."""
+ schema.pop("default", None)
+
+
+# These fields are absent in metadata=only responses. None is an internal
+# validation default, not a value returned for these fields when present.
class DocumentDetailResponse(ResponseModel):
id: str
- position: int | SkipJsonSchema[None] = None
- data_source_type: str | SkipJsonSchema[None] = None
- data_source_info: dict[str, Any] | SkipJsonSchema[None] = None
+ position: int | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
+ data_source_type: str | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
+ data_source_info: dict[str, Any] | SkipJsonSchema[None] = Field(
+ default=None, json_schema_extra=_omit_schema_default
+ )
dataset_process_rule_id: str | None = None
- dataset_process_rule: dict[str, Any] | SkipJsonSchema[None] = None
- document_process_rule: dict[str, Any] | SkipJsonSchema[None] = None
- name: str | SkipJsonSchema[None] = None
- created_from: str | SkipJsonSchema[None] = None
- created_by: str | SkipJsonSchema[None] = None
- created_at: int | SkipJsonSchema[None] = None
+ dataset_process_rule: dict[str, Any] | SkipJsonSchema[None] = Field(
+ default=None, json_schema_extra=_omit_schema_default
+ )
+ document_process_rule: dict[str, Any] | SkipJsonSchema[None] = Field(
+ default=None, json_schema_extra=_omit_schema_default
+ )
+ name: str | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
+ created_from: str | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
+ created_by: str | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
+ created_at: int | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
tokens: int | None = None
- indexing_status: str | SkipJsonSchema[None] = None
+ indexing_status: str | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
completed_at: int | None = None
updated_at: int | None = None
indexing_latency: float | None = None
error: str | None = None
- enabled: bool | SkipJsonSchema[None] = None
+ enabled: bool | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
disabled_at: int | None = None
disabled_by: str | None = None
- archived: bool | SkipJsonSchema[None] = None
+ archived: bool | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
doc_type: str | None = None
doc_metadata: list[DocumentMetadataResponse] | dict[str, Any] | None = None
- segment_count: int | SkipJsonSchema[None] = None
- average_segment_length: int | float | SkipJsonSchema[None] = None
- hit_count: int | SkipJsonSchema[None] = None
+ segment_count: int | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
+ average_segment_length: int | float | SkipJsonSchema[None] = Field(
+ default=None, json_schema_extra=_omit_schema_default
+ )
+ hit_count: int | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
display_status: str | None = None
- doc_form: str | SkipJsonSchema[None] = None
+ doc_form: str | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
doc_language: str | None = None
summary_index_status: str | None = None
- need_summary: bool | SkipJsonSchema[None] = None
+ need_summary: bool | SkipJsonSchema[None] = Field(default=None, json_schema_extra=_omit_schema_default)
@field_validator("data_source_type", "indexing_status", "display_status", "doc_form", mode="before")
@classmethod
@@ -1452,7 +1470,6 @@ class DocumentApi(DatasetApiResource):
tags=["Documents"],
responses={
204: "Success.",
- 400: "`document_indexing` : Cannot delete document during indexing.",
403: "`archived_document_immutable` : The archived document is not editable.",
404: "`not_found` : Document Not Exists.",
},
diff --git a/api/controllers/service_api/schema.py b/api/controllers/service_api/schema.py
index 393456c0e69..b42ea1c8d7f 100644
--- a/api/controllers/service_api/schema.py
+++ b/api/controllers/service_api/schema.py
@@ -16,8 +16,8 @@ from pydantic import BaseModel, WithJsonSchema
from libs.flask_restx_compat import BINARY_RESPONSE_MEDIA_TYPES_VENDOR_KEY
USER_DESCRIPTION = (
- "User identifier, unique within the application. This identifier scopes data access; resources created with "
- "one `user` value are only visible when queried with the same `user` value."
+ "End-user identifier, defined by your app and unique within it. Identifies the end user for this request. "
+ "See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules."
)
SCOPED_TASK_STOP_USER_DESCRIPTION = (
"End-user identifier, defined by your app and unique within it. Send the same `user` value used for the original "
diff --git a/api/openapi/markdown/service-openapi.md b/api/openapi/markdown/service-openapi.md
index a4b57a96b56..679d04605e9 100644
--- a/api/openapi/markdown/service-openapi.md
+++ b/api/openapi/markdown/service-openapi.md
@@ -181,7 +181,6 @@ Retrieves the status of an asynchronous annotation reply configuration job start
| 400 | `invalid_param` : The specified job does not exist. | |
| 401 | Unauthorized - invalid API token | |
| 403 | Forbidden - token scope, app, dataset, or workspace access denied | |
-| 404 | Job not found | |
### [GET] /apps/annotations
**List Annotations**
@@ -560,7 +559,6 @@ Send a request to the text generation application.
| 400 | - `app_unavailable` : App unavailable or misconfigured. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | |
| 401 | Unauthorized - invalid API token | |
| 403 | Forbidden - token scope, app, dataset, or workspace access denied | |
-| 404 | Conversation not found | |
| 429 | `too_many_requests` : Too many concurrent requests for this app. | |
| 500 | `internal_server_error` : Internal server error. | |
@@ -798,7 +796,7 @@ Create a new empty knowledge base. After creation, use [Create Document by Text]
### [DELETE] /datasets/{dataset_id}
**Delete Knowledge Base**
-Permanently delete a knowledge base and all its documents. The knowledge base must not be in use by any application.
+Permanently delete a knowledge base and all its documents.
#### Parameters
@@ -814,7 +812,6 @@ Permanently delete a knowledge base and all its documents. The knowledge base mu
| 401 | Unauthorized - invalid API token |
| 403 | Forbidden - dataset API access or workspace access denied |
| 404 | `not_found` : Dataset not found. |
-| 409 | `dataset_in_use` : The knowledge base is being used by some apps. Please remove it from the apps before deleting. |
### [GET] /datasets/{dataset_id}
**Get Knowledge Base**
@@ -1370,7 +1367,6 @@ Permanently delete a document and all its chunks from the knowledge base.
| Code | Description |
| ---- | ----------- |
| 204 | Success. |
-| 400 | `document_indexing` : Cannot delete document during indexing. |
| 401 | Unauthorized - invalid API token |
| 403 | `archived_document_immutable` : The archived document is not editable. |
| 404 | `not_found` : Document Not Exists. |
@@ -1386,7 +1382,7 @@ Retrieve detailed information about a specific document, including its indexing
| ---- | ---------- | ----------- | -------- | ------ |
| dataset_id | path | Knowledge base ID. | Yes | string (uuid) |
| document_id | path | Document ID. | Yes | string (uuid) |
-| metadata | query | `all` returns all fields including metadata. `only` returns only `id`, `doc_type`, and `doc_metadata`. `without` returns all fields except `doc_metadata`. | No | string,
**Available values:** "all", "only", "without",
**Default:** all |
+| metadata | query | `all` returns all fields including metadata. `only` returns only `id`, `doc_type`, and `doc_metadata`. `without` returns all fields except `doc_type` and `doc_metadata`. | No | string,
**Available values:** "all", "only", "without",
**Default:** all |
#### Responses
@@ -2093,9 +2089,9 @@ Retrieve basic information about this application, including name, description,
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Basic information of the application. | **application/json**: [AppInfoResponse](#appinforesponse)
|
+| 400 | `app_unavailable` : App unavailable or misconfigured. | |
| 401 | Unauthorized - invalid API token | |
| 403 | Forbidden - token scope, app, dataset, or workspace access denied | |
-| 404 | Application not found | |
### [GET] /meta
**Get App Meta**
@@ -2107,9 +2103,9 @@ Retrieve metadata about this application, including tool icons and other configu
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Successfully retrieved application meta information. | **application/json**: [AppMetaResponse](#appmetaresponse)
|
+| 400 | `app_unavailable` : App unavailable or misconfigured. | |
| 401 | Unauthorized - invalid API token | |
| 403 | Forbidden - token scope, app, dataset, or workspace access denied | |
-| 404 | Application not found | |
### [GET] /parameters
**Get App Parameters**
@@ -2124,7 +2120,6 @@ Retrieve the application's input form configuration, including feature switches,
| 400 | `app_unavailable` : App unavailable or misconfigured. | |
| 401 | Unauthorized - invalid API token | |
| 403 | Forbidden - token scope, app, dataset, or workspace access denied | |
-| 404 | Application not found | |
### [GET] /site
**Get App WebApp Settings**
@@ -2212,8 +2207,7 @@ Execute a workflow. Cannot be executed without a published workflow.
| 400 | - `not_workflow_app` : App mode does not match the API route. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Invalid parameter value. | |
| 401 | Unauthorized - invalid API token | |
| 403 | - `forbidden` : Token scope, app, or workspace access denied. - `trigger_workflow_service_mode_unavailable` : Trigger-entry workflows cannot be invoked through Web App, Service API, OpenAPI, or MCP. | |
-| 404 | Workflow not found | |
-| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | |
+| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit or the Dify Cloud workflow execution quota was exceeded. | |
| 500 | `internal_server_error` : Internal server error. | |
### [GET] /workflows/run/{workflow_run_id}
@@ -2289,7 +2283,7 @@ Execute a specific workflow version identified by its ID. Useful for running a p
| 401 | Unauthorized - invalid API token | |
| 403 | - `forbidden` : Token scope, app, or workspace access denied. - `workflow_version_execution_not_allowed` : Workflow version execution is unavailable on the current plan. Upgrade to a paid plan. - `trigger_workflow_service_mode_unavailable` : The selected workflow version uses a trigger entry and cannot be invoked through Web App, Service API, OpenAPI, or MCP. | |
| 404 | `not_found` : Workflow not found. | |
-| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | |
+| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit or the Dify Cloud workflow execution quota was exceeded. | |
| 500 | `internal_server_error` : Internal server error. | |
---
@@ -2600,7 +2594,7 @@ Public pause reason emitted by a blocking Chatflow execution.
| inputs | object | Values for app-defined variables. Refer to the `user_input_form` field in the [Get App Parameters](/api-reference/applications/get-app-parameters) response to discover expected variable names and types. | Yes |
| query | string | User input or question content. | Yes |
| response_mode | string,
**Available values:** "blocking", "streaming" | Response mode. `streaming` uses Server-Sent Events; `blocking` returns after completion. New Agent app mode supports streaming only. When omitted, non-Agent apps run in blocking mode and new Agent apps stream. | No |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | Yes |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | Yes |
| workflow_id | string | Published workflow version ID to execute for advanced chat. If omitted, the app's current published workflow is used. | No |
#### ChildChunkCreatePayload
@@ -2682,7 +2676,7 @@ Public pause reason emitted by a blocking Chatflow execution.
| inputs | object | Values for app-defined variables. Refer to the `user_input_form` field in the [Get App Parameters](/api-reference/applications/get-app-parameters) response to discover expected variable names and types. | Yes |
| query | string | User input or prompt content. | No |
| response_mode | string,
**Available values:** "blocking", "streaming" | Response mode. `streaming` uses Server-Sent Events; `blocking` returns after completion. When omitted, the request runs in blocking mode. | No |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | Yes |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | Yes |
#### Condition
@@ -2723,7 +2717,7 @@ Condition detail
| ---- | ---- | ----------- | -------- |
| auto_generate | boolean | Automatically generate the conversation name. When `true`, the `name` field is ignored. | No |
| name | string | Conversation name. Required when `auto_generate` is `false`. | No |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | No |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | No |
#### ConversationVariableInfiniteScrollPaginationResponse
@@ -2755,7 +2749,7 @@ Condition detail
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | No |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | No |
| value | | The new value for the variable. Must match the variable's expected type. | Yes |
#### ConversationVariablesQuery
@@ -2833,7 +2827,6 @@ Enum class for custom configuration status.
| maintainer | string | | No |
| name | string | | Yes |
| permission | string | | Yes |
-| permission_keys | [ string ] | | No |
| pipeline_id | string | | Yes |
| provider | string | | Yes |
| retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes |
@@ -2876,7 +2869,6 @@ Enum class for custom configuration status.
| name | string | | Yes |
| partial_member_list | [ string ] | | No |
| permission | string | | Yes |
-| permission_keys | [ string ] | | No |
| pipeline_id | string | | Yes |
| provider | string | | Yes |
| retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | Retrieval configuration for the knowledge base. | Yes |
@@ -3158,7 +3150,7 @@ Request payload for bulk downloading documents as a zip archive.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
-| metadata | string,
**Available values:** "all", "only", "without",
**Default:** all | `all` returns all fields including metadata. `only` returns only `id`, `doc_type`, and `doc_metadata`. `without` returns all fields except `doc_metadata`.
*Enum:* `"all"`, `"only"`, `"without"` | No |
+| metadata | string,
**Available values:** "all", "only", "without",
**Default:** all | `all` returns all fields including metadata. `only` returns only `id`, `doc_type`, and `doc_metadata`. `without` returns all fields except `doc_type` and `doc_metadata`.
*Enum:* `"all"`, `"only"`, `"without"` | No |
#### DocumentListQuery
@@ -3546,7 +3538,7 @@ Enum class for fetch from.
| ---- | ---- | ----------- | -------- |
| action | string | ID of the action button the recipient selected. Must match one of the `id` values from the form's `user_actions` list. | Yes |
| inputs | object | Submitted human input values keyed by output variable name. Use a string for paragraph or select input values, a file mapping for file inputs, and a list of file mappings for file-list inputs. Local file mappings use `transfer_method=local_file` with `upload_file_id`; remote file mappings use `transfer_method=remote_url` with `url` or `remote_url`. | Yes |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | Yes |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | Yes |
#### HumanInputFormSubmitResponse
@@ -3622,7 +3614,7 @@ Model class for i18n object.
| ---- | ---- | ----------- | -------- |
| content | string | Optional text feedback providing additional detail. | No |
| rating | string,
**Available values:** "dislike", "like" | Feedback rating. Set to `null` to revoke previously submitted feedback. | No |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | Yes |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | Yes |
#### MessageFile
@@ -3753,7 +3745,7 @@ Enum class for model type.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | No |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | No |
#### ParagraphInputConfig
@@ -4253,7 +4245,7 @@ Accepts either the legacy tag_id payload or the normalized tag_ids payload.
| message_id | string | Message ID. Takes priority over `text` when both are provided. | No |
| streaming | boolean | Reserved for compatibility; TTS response streaming is determined by the provider output. | No |
| text | string | Speech content to convert. | No |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | No |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | No |
| voice | string | Voice to use for text-to-speech. Available voices depend on the TTS provider configured for this app. Omit to use the app's configured voice when available; that value is exposed by [Get App Parameters](/api-reference/applications/get-app-parameters) as `text_to_speech.voice`. | No |
#### ToolIcon
@@ -4461,7 +4453,7 @@ Public pause reason emitted by a blocking Workflow execution.
| files | [ object
object
object
object ] | File list for workflow system file inputs. Available when file upload is enabled for the workflow. To attach a local file, first upload it via [Upload File](/api-reference/files/upload-file) and use the returned `id` as `upload_file_id` with `transfer_method: local_file`. | No |
| inputs | object | Key-value pairs for workflow input variables. Values for file-type variables should be arrays of file objects with `type`, `transfer_method`, and either `url` or `upload_file_id`. Refer to the `user_input_form` field in the [Get App Parameters](/api-reference/applications/get-app-parameters) response to discover the variable names and types expected by your app. | Yes |
| response_mode | string,
**Available values:** "blocking", "streaming" | Response mode. Use `blocking` for synchronous responses or `streaming` for Server-Sent Events. When omitted, the request runs in blocking mode. | No |
-| user | string | User identifier, unique within the application. This identifier scopes data access; resources created with one `user` value are only visible when queried with the same `user` value. | Yes |
+| user | string | End-user identifier, defined by your app and unique within it. Identifies the end user for this request. See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules. | Yes |
#### WorkflowRunResponse
diff --git a/api/tests/unit_tests/commands/test_generate_swagger_specs.py b/api/tests/unit_tests/commands/test_generate_swagger_specs.py
index 0c82e704572..34147acde8e 100644
--- a/api/tests/unit_tests/commands/test_generate_swagger_specs.py
+++ b/api/tests/unit_tests/commands/test_generate_swagger_specs.py
@@ -195,6 +195,19 @@ def test_generate_specs_writes_openapi_with_resolvable_references_and_null_defau
assert "default" in conversation_id
assert conversation_id["default"] is None
+ schemas = service_payload["components"]["schemas"]
+ document_detail = schemas["DocumentDetailResponse"]
+ validator = Draft202012Validator(service_payload)
+ for schema in (document_detail, *schemas["DocumentTextUpdate"]["anyOf"]):
+ for property_schema in schema["properties"].values():
+ if "default" in property_schema:
+ validator.evolve(schema=property_schema).validate(property_schema["default"])
+
+ assert document_detail["required"] == ["id"]
+ assert document_detail["properties"]["enabled"]["type"] == "boolean"
+ assert "default" not in document_detail["properties"]["enabled"]
+ assert document_detail["properties"]["tokens"]["default"] is None
+
def test_generate_specs_writes_unique_operation_ids(tmp_path: Path):
module = _load_generate_swagger_specs_module()
diff --git a/api/tests/unit_tests/controllers/test_swagger.py b/api/tests/unit_tests/controllers/test_swagger.py
index 6f9f4677bb4..de0845c9ebd 100644
--- a/api/tests/unit_tests/controllers/test_swagger.py
+++ b/api/tests/unit_tests/controllers/test_swagger.py
@@ -15,8 +15,8 @@ def _swagger_config(config_overrides) -> None:
USER_PROPERTY_SCHEMA = {
"description": (
- "User identifier, unique within the application. This identifier scopes data access; resources created with "
- "one `user` value are only visible when queried with the same `user` value."
+ "End-user identifier, defined by your app and unique within it. Identifies the end user for this request. "
+ "See [End User Identity](/api-reference/guides/end-user-identity) for endpoint-specific access rules."
),
"type": "string",
}
@@ -298,6 +298,11 @@ def test_service_openapi_documents_decorator_user_contracts():
assert schema["properties"]["user"] == USER_PROPERTY_SCHEMA
assert "user" in schema["required"]
+ for path in ("/workflows/run", "/workflows/{workflow_id}/run"):
+ rate_limit_description = paths[path]["post"]["responses"]["429"]["description"]
+ assert "upstream model provider rate limit" in rate_limit_description
+ assert "Dify Cloud workflow execution quota" in rate_limit_description
+
task_stop_user_descriptions = {
"/completion-messages/{task_id}/stop": "Send the same",
"/chat-messages/{task_id}/stop": "Send the same",
@@ -477,10 +482,75 @@ def test_service_openapi_documents_conditional_payload_schemas():
with_text_branch, without_text_branch = document_update_schema["anyOf"]
assert with_text_branch["properties"]["text"]["type"] == "string"
assert with_text_branch["properties"]["name"]["type"] == "string"
+ assert "default" not in with_text_branch["properties"]["text"]
+ assert "default" not in with_text_branch["properties"]["name"]
assert with_text_branch["required"] == ["name", "text"]
assert without_text_branch["properties"]["text"]["type"] == "null"
+def test_service_dataset_response_schemas_omit_console_permission_metadata():
+ from controllers.console import bp as console_bp
+ from controllers.service_api import bp as service_api_bp
+
+ app = Flask(__name__)
+ app.config["TESTING"] = True
+ app.config["RESTX_INCLUDE_ALL_MODELS"] = True
+ app.register_blueprint(console_bp)
+ app.register_blueprint(service_api_bp)
+
+ service_payload = app.test_client().get("/v1/openapi.json").get_json()
+ service_schemas = service_payload["components"]["schemas"]
+ for name in ("DatasetDetailResponse", "DatasetDetailWithPartialMembersResponse"):
+ assert "permission_keys" not in service_schemas[name]["properties"]
+ assert service_schemas["DatasetListResponse"]["properties"]["data"]["items"] == {
+ "$ref": "#/components/schemas/DatasetDetailResponse"
+ }
+
+ console_payload = app.test_client().get("/console/api/openapi.json").get_json()
+ console_schema = console_payload["components"]["schemas"]["DatasetDetailResponse"]
+ assert "permission_keys" in console_schema["properties"]
+
+
+def test_service_delete_schemas_omit_unenforced_state_constraints():
+ from controllers.service_api import bp as service_api_bp
+
+ app = Flask(__name__)
+ app.config["TESTING"] = True
+ app.register_blueprint(service_api_bp)
+ paths = app.test_client().get("/v1/openapi.json").get_json()["paths"]
+
+ delete_dataset = paths["/datasets/{dataset_id}"]["delete"]
+ assert "409" not in delete_dataset["responses"]
+ assert "must not be in use" not in delete_dataset["description"]
+
+ delete_document = paths["/datasets/{dataset_id}/documents/{document_id}"]["delete"]
+ assert "document_indexing" not in json.dumps(delete_document["responses"])
+ assert "archived_document_immutable" in delete_document["responses"]["403"]["description"]
+
+
+def test_service_schemas_only_document_reachable_not_found_responses():
+ from controllers.service_api import bp as service_api_bp
+
+ app = Flask(__name__)
+ app.config["TESTING"] = True
+ app.register_blueprint(service_api_bp)
+ paths = app.test_client().get("/v1/openapi.json").get_json()["paths"]
+
+ for path, method in (
+ ("/apps/annotation-reply/{action}/status/{job_id}", "get"),
+ ("/info", "get"),
+ ("/meta", "get"),
+ ("/parameters", "get"),
+ ("/workflows/run", "post"),
+ ("/completion-messages", "post"),
+ ):
+ assert "404" not in paths[path][method]["responses"]
+ assert "400" in paths[path][method]["responses"]
+
+ assert "404" in paths["/workflows/{workflow_id}/run"]["post"]["responses"]
+ assert "404" in paths["/chat-messages"]["post"]["responses"]
+
+
def test_service_openapi_does_not_encode_docs_coverage_boundaries():
from controllers.service_api import bp as service_api_bp
diff --git a/packages/contracts/generated/api/service/orpc.gen.ts b/packages/contracts/generated/api/service/orpc.gen.ts
index d491e4de72d..de2363aad6d 100644
--- a/packages/contracts/generated/api/service/orpc.gen.ts
+++ b/packages/contracts/generated/api/service/orpc.gen.ts
@@ -1888,12 +1888,11 @@ export const tags2 = {
/**
* Delete Knowledge Base
*
- * Permanently delete a knowledge base and all its documents. The knowledge base must not be in use by any application.
+ * Permanently delete a knowledge base and all its documents.
*/
export const delete8 = oc
.route({
- description:
- 'Permanently delete a knowledge base and all its documents. The knowledge base must not be in use by any application.',
+ description: 'Permanently delete a knowledge base and all its documents.',
inputStructure: 'detailed',
method: 'DELETE',
operationId: 'deleteDatasetsByDatasetId',
diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts
index e0a115687f0..b51d4c34d93 100644
--- a/packages/contracts/generated/api/service/types.gen.ts
+++ b/packages/contracts/generated/api/service/types.gen.ts
@@ -585,7 +585,6 @@ export type DatasetDetailResponse = {
maintainer?: string | null
name: string
permission: string
- permission_keys?: Array
pipeline_id: string | null
provider: string
retrieval_model_dict: DatasetRetrievalModelResponse
@@ -626,7 +625,6 @@ export type DatasetDetailWithPartialMembersResponse = {
name: string
partial_member_list?: Array | null
permission: string
- permission_keys?: Array
pipeline_id: string | null
provider: string
retrieval_model_dict: DatasetRetrievalModelResponse
@@ -2492,7 +2490,6 @@ export type GetAppsAnnotationReplyByActionStatusByJobIdErrors = {
400: unknown
401: unknown
403: unknown
- 404: unknown
}
export type GetAppsAnnotationReplyByActionStatusByJobIdResponses = {
@@ -2668,7 +2665,6 @@ export type PostCompletionMessagesErrors = {
400: unknown
401: unknown
403: unknown
- 404: unknown
429: unknown
500: unknown
}
@@ -3023,7 +3019,6 @@ export type DeleteDatasetsByDatasetIdErrors = {
401: unknown
403: unknown
404: unknown
- 409: unknown
}
export type DeleteDatasetsByDatasetIdResponses = {
@@ -3312,7 +3307,6 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdData = {
}
export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdErrors = {
- 400: unknown
401: unknown
403: unknown
404: unknown
@@ -4162,9 +4156,9 @@ export type GetInfoData = {
}
export type GetInfoErrors = {
+ 400: unknown
401: unknown
403: unknown
- 404: unknown
}
export type GetInfoResponses = {
@@ -4255,9 +4249,9 @@ export type GetMetaData = {
}
export type GetMetaErrors = {
+ 400: unknown
401: unknown
403: unknown
- 404: unknown
}
export type GetMetaResponses = {
@@ -4277,7 +4271,6 @@ export type GetParametersErrors = {
400: unknown
401: unknown
403: unknown
- 404: unknown
}
export type GetParametersResponses = {
@@ -4390,7 +4383,6 @@ export type PostWorkflowsRunErrors = {
400: unknown
401: unknown
403: unknown
- 404: unknown
429: unknown
500: unknown
}
diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts
index 6ff23f78936..c04a4650041 100644
--- a/packages/contracts/generated/api/service/zod.gen.ts
+++ b/packages/contracts/generated/api/service/zod.gen.ts
@@ -784,7 +784,6 @@ export const zDatasetDetailResponse = z.object({
maintainer: z.string().nullish(),
name: z.string(),
permission: z.string(),
- permission_keys: z.array(z.string()).optional(),
pipeline_id: z.string().nullable(),
provider: z.string(),
retrieval_model_dict: zDatasetRetrievalModelResponse,
@@ -828,7 +827,6 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({
name: z.string(),
partial_member_list: z.array(z.string()).nullish(),
permission: z.string(),
- permission_keys: z.array(z.string()).optional(),
pipeline_id: z.string().nullable(),
provider: z.string(),
retrieval_model_dict: zDatasetRetrievalModelResponse,