fix(api): align service api schemas with runtime behavior (#41943)

This commit is contained in:
Stephen Zhou 2026-09-08 03:59:02 +00:00 committed by GitHub
parent 146b193ef7
commit 7ba2cfb30b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 177 additions and 86 deletions

View File

@ -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.

View File

@ -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(

View File

@ -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(

View File

@ -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",
}
)

View File

@ -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.",
},

View File

@ -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")

View File

@ -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.",
},

View File

@ -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 "

View File

@ -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, <br>**Available values:** "all", "only", "without", <br>**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, <br>**Available values:** "all", "only", "without", <br>**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)<br> |
| 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)<br> |
| 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, <br>**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, <br>**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, <br>**Available values:** "all", "only", "without", <br>**Default:** all | `all` returns all fields including metadata. `only` returns only `id`, `doc_type`, and `doc_metadata`. `without` returns all fields except `doc_metadata`.<br>*Enum:* `"all"`, `"only"`, `"without"` | No |
| metadata | string, <br>**Available values:** "all", "only", "without", <br>**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`.<br>*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, <br>**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<br>object<br>object<br>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, <br>**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

View File

@ -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()

View File

@ -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

View File

@ -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',

View File

@ -585,7 +585,6 @@ export type DatasetDetailResponse = {
maintainer?: string | null
name: string
permission: string
permission_keys?: Array<string>
pipeline_id: string | null
provider: string
retrieval_model_dict: DatasetRetrievalModelResponse
@ -626,7 +625,6 @@ export type DatasetDetailWithPartialMembersResponse = {
name: string
partial_member_list?: Array<string> | null
permission: string
permission_keys?: Array<string>
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
}

View File

@ -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,