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,