Custom Tool OpenAPI Integration#
Dify's custom tool system (ApiTool) lets users register arbitrary HTTP APIs by supplying an OpenAPI 3.x, Swagger 2.x, or OpenAI plugin JSON schema. At registration time the schema is parsed into one or more ApiToolBundle objects β one per operation β each carrying a frozen server_url, HTTP method, parameter list, and the raw OpenAPI operation dict. These bundles are persisted to the database and drive every subsequent tool invocation.
Parsing Pipeline#
Entry point: ApiBasedToolSchemaParser.auto_parse_to_tool_bundle() in api/core/tools/utils/parser.py.
The function auto-detects format and attempts three parse strategies in order :
- OpenAPI 3.x β direct parse via
parse_openapi_to_tool_bundle() - Swagger 2.x β upgraded to OpenAPI 3.0 by
parse_swagger_to_openapi(), then re-parsed - OpenAI plugin JSON β fetches the referenced OpenAPI YAML over HTTP and re-parses
The service layer entry is ApiToolManageService.parser_api_schema(), which calls auto_parse_to_tool_bundle() and wraps the result with a credentials schema (auth type, api key header, api key value).
Server URL Extraction#
Inside parse_openapi_to_tool_bundle(), server URL selection follows this logic :
- Default to
servers[0]["url"]. - If a Flask request context is active and the
X-Request-Envheader is set, scanserversfor the first entry whose"env"field matches and use that URL instead. - Fall back to
servers[0]["url"]when no matching"env"entry is found.
The "env" field is a non-standard Dify extension β standard OpenAPI specs don't define it. Accessing it safely requires server.get("env") rather than server["env"]; the bracket-access form caused a KeyError on standard schemas whenever X-Request-Env was present, fixed in PR #42025.
For each path + method combination, the resolved server_url is concatenated with the path string and stored as ApiToolBundle.server_url β a plain str field baked in at parse time .
Invocation Flow#
At runtime, ApiTool._invoke() passes self.api_bundle.server_url directly to do_http_request(). Inside that method:
- Path parameters (
in: path) are interpolated via string replacement:url.replace(f"{{{name}}}", f"{value}"). The host portion of the URL is never touched. - Query / header / cookie parameters are merged into the request.
- Auth is assembled by
assembling_request(): supportsnone,api_key_header(withBasic/Bearer/customprefixes), andapi_key_querymodes. - All requests are dispatched via
ssrf_proxyto prevent SSRF .
Limitations on Dynamic Parameterization#
| Limitation | Detail |
|---|---|
| No runtime server URL override | ApiToolBundle.server_url is a plain str set once at parse time . No per-invocation substitution mechanism exists. |
OpenAPI variables not supported | The servers[].variables templating spec (e.g. https://{tenant}.example.com) is silently ignored; the raw URL string is used verbatim . |
| Only one server active per request | Multi-environment selection via X-Request-Env picks at most one server; all candidate URLs must be pre-declared in the schema. |
| Path parameters only | Only {param} tokens in the URL path are interpolated at runtime . The scheme and host are static. |
Multi-Environment Server Extension#
To route the same tool bundle to different backends (e.g. staging vs. production), add multiple servers entries with a custom "env" key:
servers:
- url: https://api.example.com
- url: https://staging.example.com
env: staging
Send X-Request-Env: staging on the parse request to select the staging URL. If no server matches, the parser falls back to servers[0] .
Key Files#
| File | Role |
|---|---|
api/core/tools/utils/parser.py | Schema parsing; server URL extraction; bundle construction |
api/core/tools/entities/tool_bundle.py | ApiToolBundle data model |
api/core/tools/custom_tool/tool.py | ApiTool invocation, auth assembly, HTTP dispatch |
api/services/tools/api_tools_manage_service.py | Service layer: schema parsing, CRUD for API tool providers |