From 7c5a999e32af3406554c80ee2f4013955aa4968b Mon Sep 17 00:00:00 2001 From: easyai Date: Wed, 22 Jul 2026 08:48:32 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(api):=20=E7=BB=9F=E4=B8=80=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E6=8E=A5=E5=8F=A3=E4=B8=BA=20/api/v1=20=E5=89=8D?= =?UTF-8?q?=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将通用生成、Gemini、可灵、火山、健康检查与 OpenAPI 的推荐入口统一到 /api/v1,并保留历史路径作为兼容别名。同步更新代理配置、接入文档、接口清单和前缀回归测试。\n\n验证:go vet ./...;go test ./...;pnpm openapi;pnpm lint;pnpm test;pnpm build;公开 OpenAPI 71 个方法与接口清单机器比对一致。 --- README.md | 8 +- apps/api/docs/swagger.json | 4142 +++++------------ apps/api/docs/swagger.yaml | 2709 +++-------- .../httpapi/agent_resources_handlers.go | 8 +- .../httpapi/agent_resources_handlers_test.go | 8 +- .../httpapi/chat_completions_mode_test.go | 21 +- .../httpapi/core_flow_integration_test.go | 16 +- .../internal/httpapi/file_upload_handlers.go | 1 - apps/api/internal/httpapi/gemini_compat.go | 48 +- .../internal/httpapi/gemini_compat_test.go | 12 + apps/api/internal/httpapi/handlers.go | 32 +- .../httpapi/keling_compat_handlers.go | 4 +- .../httpapi/keling_compat_integration_test.go | 6 +- .../keling_simulation_integration_test.go | 3 +- apps/api/internal/httpapi/kling_compat.go | 21 +- .../httpapi/kling_compat_integration_test.go | 14 +- apps/api/internal/httpapi/openapi_models.go | 4 +- .../httpapi/public_api_prefix_test.go | 72 + apps/api/internal/httpapi/server.go | 24 +- .../internal/httpapi/voice_clone_handlers.go | 4 - .../httpapi/volces_compat_handlers.go | 10 +- .../skills/ai-gateway-ops-management/SKILL.md | 2 +- .../references/api-discovery-and-safety.md | 15 +- .../references/model-acceptance-runbook.md | 6 +- apps/api/project.json | 2 +- apps/web/src/api.test.ts | 4 +- apps/web/src/pages/ApiDocsPage.tsx | 22 +- deploy/nginx/ai.51easyai.com-api-v1.inc | 19 + docker-compose.yml | 4 +- docker/nginx.conf | 33 + docs/design.md | 8 +- docs/kling-compatible-api.md | 4 +- docs/kling-omni-compatible.md | 18 +- docs/public-api-v1.md | 137 + docs/runbooks/production-ci-cd.md | 6 +- docs/seedance-portrait-assets.md | 10 +- docs/server-main-integration.md | 2 +- docs/test/loopback-test-checklist.md | 4 +- 38 files changed, 2294 insertions(+), 5169 deletions(-) create mode 100644 apps/api/internal/httpapi/public_api_prefix_test.go create mode 100644 deploy/nginx/ai.51easyai.com-api-v1.inc create mode 100644 docs/public-api-v1.md diff --git a/README.md b/README.md index ef73671..edf5227 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,10 @@ scripts/deploy-compose.sh 部署成功后默认访问地址: - Web: `http://127.0.0.1:5178` -- API: `http://127.0.0.1:8088/healthz` -- Web 反代 API: `http://127.0.0.1:5178/gateway-api/healthz` +- API: `http://127.0.0.1:8088/api/v1/healthz` +- Web 反代公开 API: `http://127.0.0.1:5178/api/v1/healthz` + +公开接口统一使用 `/api/v1` 前缀,完整分组清单见 [公开 API V1 清单](docs/public-api-v1.md)。 常用覆盖项: @@ -99,7 +101,7 @@ scripts/deploy-compose.sh clean docker login --username= registry.cn-shanghai.aliyuncs.com ``` -Web 容器的 Nginx 配置通过 bind mount 挂载自仓库文件 [docker/nginx.conf](docker/nginx.conf),可直接修改该文件调整静态资源和 `/gateway-api` 反向代理配置。修改后执行以下命令使配置生效: +Web 容器的 Nginx 配置通过 bind mount 挂载自仓库文件 [docker/nginx.conf](docker/nginx.conf),可直接修改该文件调整静态资源、规范 `/api/v1` 公开入口和旧 `/gateway-api` 兼容反向代理。修改后执行以下命令使配置生效: ```bash docker compose -f docker-compose.yml restart web diff --git a/apps/api/docs/swagger.json b/apps/api/docs/swagger.json index 7a9cc57..fc68149 100644 --- a/apps/api/docs/swagger.json +++ b/apps/api/docs/swagger.json @@ -12,47 +12,6 @@ }, "basePath": "/", "paths": { - "/api-docs-json": { - "get": { - "description": "返回当前构建内嵌的完整机器可读 Swagger JSON,供 Agent 在 SKILL references 未覆盖接口时查询。", - "produces": [ - "application/json" - ], - "tags": [ - "agent-resources" - ], - "summary": "获取 AI Gateway Swagger JSON", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - } - }, - "/api-docs-yaml": { - "get": { - "description": "返回当前构建内嵌的完整机器可读 Swagger YAML。", - "produces": [ - "application/yaml" - ], - "tags": [ - "agent-resources" - ], - "summary": "获取 AI Gateway Swagger YAML", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "string" - } - } - } - } - }, "/api/admin/access-rules": { "get": { "security": [ @@ -4737,6 +4696,31 @@ } } }, + "/api/v1/ai/result/{taskID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "volces-compatible" + ], + "summary": "查询 server-main 兼容视频结果", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/api-keys": { "get": { "security": [ @@ -5518,6 +5502,107 @@ } } }, + "/api/v1/contents/generations/tasks": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "volces-compatible" + ], + "summary": "列出火山内容生成任务", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "统一公开入口兼容火山方舟内容生成任务。网关 task id 是查询与取消用的公开 id;上游 id 另以 upstream_task_id 保留。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "volces-compatible" + ], + "summary": "创建火山内容生成任务", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/contents/generations/tasks/{taskID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "volces-compatible" + ], + "summary": "查询火山内容生成任务", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "取消网关任务;对于已提交且保存了上游任务标识的 Volces 视频任务,同时调用火山 DELETE 接口并持久化取消状态。", + "produces": [ + "application/json" + ], + "tags": [ + "volces-compatible" + ], + "summary": "取消火山内容生成任务", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/embeddings": { "post": { "security": [ @@ -5525,7 +5610,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -5685,6 +5770,132 @@ } } }, + "/api/v1/gemini/upload/{version}/files": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "使用统一 /api/v1 前缀启动或直接完成 Gemini Files 上传。", + "consumes": [ + "application/octet-stream" + ], + "produces": [ + "application/json" + ], + "tags": [ + "gemini-compatible" + ], + "summary": "Gemini Files 上传接口", + "parameters": [ + { + "type": "string", + "description": "Gemini 版本(v1 或 v1beta)", + "name": "version", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/api/v1/gemini/upload/{version}/files/{uploadID}": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/octet-stream" + ], + "produces": [ + "application/json" + ], + "tags": [ + "gemini-compatible" + ], + "summary": "完成 Gemini Files 分段上传", + "parameters": [ + { + "type": "string", + "description": "Gemini 版本(v1 或 v1beta)", + "name": "version", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "上传会话 ID", + "name": "uploadID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/api/v1/healthz": { + "get": { + "description": "返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。", + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "健康检查", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.HealthResponse" + } + } + } + } + }, "/api/v1/images/edits": { "post": { "security": [ @@ -5692,7 +5903,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -5791,7 +6002,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -5883,6 +6094,278 @@ } } }, + "/api/v1/kling/v1/videos/omni-video": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "kling-compatible" + ], + "summary": "分页查询可灵 V1 Omni 视频任务", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "pageNum", + "in": "query" + }, + { + "type": "integer", + "default": 30, + "description": "每页数量", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "兼容中国区可灵 V1 /v1/videos/omni-video;用户使用网关 API Key,网关在服务端使用 AK/SK 调用上游。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kling-compatible" + ], + "summary": "创建可灵 V1 Omni 视频任务", + "parameters": [ + { + "description": "可灵 V1 Omni 请求", + "name": "input", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/api/v1/kling/v1/videos/omni-video/{taskID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "kling-compatible" + ], + "summary": "查询可灵 V1 Omni 视频任务", + "parameters": [ + { + "type": "string", + "description": "任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/kling/v2/omni-video/{model}": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "兼容可灵 API 2.0 的模型路径;调用方使用网关 API Key,网关转换并使用中国区 V1 AK/SK 上游。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kling-compatible" + ], + "summary": "创建可灵 API 2.0 Omni 视频任务", + "parameters": [ + { + "type": "string", + "description": "模型路径(kling-o1 或 kling-v3-omni)", + "name": "model", + "in": "path", + "required": true + }, + { + "description": "可灵 API 2.0 请求", + "name": "input", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, + "/api/v1/kling/v2/tasks": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "kling-compatible" + ], + "summary": "按 ID 查询可灵 API 2.0 任务", + "parameters": [ + { + "type": "string", + "description": "逗号分隔的任务 ID", + "name": "task_ids", + "in": "query" + }, + { + "type": "string", + "description": "逗号分隔的外部任务 ID", + "name": "external_task_ids", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kling-compatible" + ], + "summary": "分页查询可灵 API 2.0 任务", + "parameters": [ + { + "description": "游标、数量、时间范围和筛选条件", + "name": "input", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/me": { "get": { "security": [ @@ -6000,6 +6483,78 @@ } } }, + "/api/v1/models/{model}:generateContent": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "使用统一 /api/v1 前缀接收 Gemini generateContent 请求;旧 /v1 和 /v1beta 路径保留兼容。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "gemini-compatible" + ], + "summary": "Gemini generateContent 兼容接口", + "parameters": [ + { + "type": "string", + "description": "模型名称", + "name": "model", + "in": "path", + "required": true + }, + { + "description": "Gemini generateContent 请求", + "name": "input", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, "/api/v1/music/generations": { "post": { "security": [ @@ -6007,7 +6562,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -6099,6 +6654,47 @@ } } }, + "/api/v1/openapi.json": { + "get": { + "description": "返回当前构建内嵌的完整机器可读 Swagger JSON,供 Agent 在 SKILL references 未覆盖接口时查询。", + "produces": [ + "application/json" + ], + "tags": [ + "agent-resources" + ], + "summary": "获取 AI Gateway Swagger JSON", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/openapi.yaml": { + "get": { + "description": "返回当前构建内嵌的完整机器可读 Swagger YAML。", + "produces": [ + "application/yaml" + ], + "tags": [ + "agent-resources" + ], + "summary": "获取 AI Gateway Swagger YAML", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/platforms": { "get": { "security": [ @@ -6404,6 +7000,32 @@ } } }, + "/api/v1/readyz": { + "get": { + "description": "检查 Postgres 是否可用;数据库不可用时返回 503。", + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "就绪检查", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.ReadyResponse" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/httpapi.ErrorEnvelope" + } + } + } + } + }, "/api/v1/reranks": { "post": { "security": [ @@ -6411,7 +7033,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -6503,6 +7125,130 @@ } } }, + "/api/v1/resource/material": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "文件先写入网关文件存储;仅在 private_avatar_eligible=true 时登记到火山 Assets。创建后会立即触发一次状态同步。", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "portrait-assets" + ], + "summary": "上传并创建 Seedance 真人资产", + "parameters": [ + { + "type": "file", + "description": "真人资产源文件(图片、视频或音频)", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "material JSON,category 必须是 seedance_portrait_asset", + "name": "data", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/resource/material/seedance-portrait-assets/capability": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回当前网关是否已配置可创建、同步和引用的火山 Seedance 真人资产平台。", + "produces": [ + "application/json" + ], + "tags": [ + "portrait-assets" + ], + "summary": "查询 Seedance 真人资产能力", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/runner.PortraitAssetCapability" + } + } + } + } + }, + "/api/v1/resource/material/seedance-portrait-assets/sync": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "调用火山 CreateAsset/GetAsset;多次调用可把 Processing 状态刷新为 Active 或 Failed。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "portrait-assets" + ], + "summary": "同步 Seedance 真人资产状态", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/runner.PortraitAssetSyncResponse" + } + } + } + } + }, + "/api/v1/resource/material/user/materials": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "返回当前用户的真人资产;兼容 server-main material 列表响应字段。", + "produces": [ + "application/json" + ], + "tags": [ + "portrait-assets" + ], + "summary": "列出 Seedance 真人资产", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/responses": { "post": { "security": [ @@ -6660,7 +7406,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -6759,7 +7505,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -7160,6 +7906,35 @@ } } }, + "/api/v1/video/generations": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "兼容 server-main 的 /api/v1/video/generations,返回 submitted 和 task_id;额外保留火山任务字段。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "volces-compatible" + ], + "summary": "创建 server-main 兼容视频任务", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/v1/videos/generations": { "post": { "security": [ @@ -7167,7 +7942,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -7259,6 +8034,139 @@ } } }, + "/api/v1/videos/omni-video": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "兼容 Kling 旧版 /v1/videos/omni-video;Bearer token 必须为 Gateway API Key。任务固定异步执行,返回的 task_id 为网关任务 ID。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kling-compatible" + ], + "summary": "创建 Kling Omni 视频任务", + "parameters": [ + { + "description": "Kling Omni 官方兼容请求", + "name": "input", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/httpapi.KelingOmniVideoRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + } + } + } + }, + "/api/v1/videos/omni-video/{taskID}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "按创建接口返回的网关 task_id 查询任务;仅允许创建任务的 Gateway 用户访问。", + "produces": [ + "application/json" + ], + "tags": [ + "kling-compatible" + ], + "summary": "查询 Kling Omni 视频任务", + "parameters": [ + { + "type": "string", + "description": "网关任务 ID", + "name": "taskID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" + } + } + } + } + }, "/api/v1/voice_clone": { "post": { "security": [ @@ -7266,7 +8174,7 @@ "BearerAuth": [] } ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", + "description": "统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。", "consumes": [ "application/json" ], @@ -8009,1169 +8917,6 @@ } } }, - "/chat/completions": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "OpenAI-compatible Chat Completions 入口;仅接受官方字段及文档声明的 EasyAI 路由扩展,未知顶层字段返回 400 invalid_parameter。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "text/event-stream" - ], - "tags": [ - "chat" - ], - "summary": "创建 OpenAI Chat Completions", - "parameters": [ - { - "description": "Chat Completions 请求", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.ChatCompletionRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.ChatCompletionCompatibleResponse" - } - }, - "400": { - "description": "invalid_parameter", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/embeddings": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/healthz": { - "get": { - "description": "返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。", - "produces": [ - "application/json" - ], - "tags": [ - "system" - ], - "summary": "健康检查", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.HealthResponse" - } - } - } - } - }, - "/images/edits": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/images/generations": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/kling/omni-video/{model}": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "兼容可灵 API 2.0 的模型路径;调用方使用网关 API Key,网关转换并使用中国区 V1 AK/SK 上游。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "kling-compatible" - ], - "summary": "创建可灵 API 2.0 Omni 视频任务", - "parameters": [ - { - "type": "string", - "description": "模型路径(kling-o1 或 kling-v3-omni)", - "name": "model", - "in": "path", - "required": true - }, - { - "description": "可灵 API 2.0 请求", - "name": "input", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/kling/tasks": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "kling-compatible" - ], - "summary": "按 ID 查询可灵 API 2.0 任务", - "parameters": [ - { - "type": "string", - "description": "逗号分隔的任务 ID", - "name": "task_ids", - "in": "query" - }, - { - "type": "string", - "description": "逗号分隔的外部任务 ID", - "name": "external_task_ids", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "kling-compatible" - ], - "summary": "分页查询可灵 API 2.0 任务", - "parameters": [ - { - "description": "游标、数量、时间范围和筛选条件", - "name": "input", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - } - }, - "/kling/v1/videos/omni-video": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "kling-compatible" - ], - "summary": "分页查询可灵 V1 Omni 视频任务", - "parameters": [ - { - "type": "integer", - "default": 1, - "description": "页码", - "name": "pageNum", - "in": "query" - }, - { - "type": "integer", - "default": 30, - "description": "每页数量", - "name": "pageSize", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "兼容中国区可灵 V1 /v1/videos/omni-video;用户使用网关 API Key,网关在服务端使用 AK/SK 调用上游。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "kling-compatible" - ], - "summary": "创建可灵 V1 Omni 视频任务", - "parameters": [ - { - "description": "可灵 V1 Omni 请求", - "name": "input", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/kling/v1/videos/omni-video/{taskID}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "kling-compatible" - ], - "summary": "查询可灵 V1 Omni 视频任务", - "parameters": [ - { - "type": "string", - "description": "任务 ID", - "name": "taskID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - } - }, - "/music/generations": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/readyz": { - "get": { - "description": "检查 Postgres 是否可用;数据库不可用时返回 503。", - "produces": [ - "application/json" - ], - "tags": [ - "system" - ], - "summary": "就绪检查", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.ReadyResponse" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/reranks": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/responses": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "公开 OpenAI-compatible Responses 入口。模型声明 openai_responses 时原生转发,否则使用 Chat Completions 转换;store 缺省为 true。previous_response_id 严格绑定首次成功的平台模型和上游协议,链路不可用时不跨平台续接。未提供 previous_response_id 时由调用方管理完整状态,Gateway 以本轮 input/messages 为准且不追加本地历史。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "text/event-stream" - ], - "tags": [ - "responses" - ], - "summary": "创建 OpenAI Responses", - "parameters": [ - { - "description": "Responses 请求;Chat 回退只支持自定义 function tools", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.ResponsesRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.ResponsesCompatibleResponse" - }, - "headers": { - "X-Gateway-Task-Id": { - "type": "string", - "description": "网关审计任务 ID" - } - } - }, - "400": { - "description": "invalid_previous_response_id / unsupported_response_tool / unsupported_response_parameter", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "503": { - "description": "response_chain_unavailable", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/song/generations": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/speech/generations": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, "/static/generated/{asset}": { "get": { "description": "从本地生成资源目录读取图片、视频等任务产物;不存在时返回 404。", @@ -9278,1823 +9023,6 @@ } } } - }, - "/tasks": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "按当前用户列出任务,支持关键字、模型类型、时间范围和分页过滤。", - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "列出任务", - "parameters": [ - { - "type": "string", - "description": "搜索关键字,别名 query", - "name": "q", - "in": "query" - }, - { - "type": "string", - "description": "模型类型,别名 type", - "name": "modelType", - "in": "query" - }, - { - "type": "string", - "description": "创建时间起点,支持 RFC3339 或日期格式,别名 from", - "name": "createdFrom", - "in": "query" - }, - { - "type": "string", - "description": "创建时间终点,支持 RFC3339 或日期格式,别名 to", - "name": "createdTo", - "in": "query" - }, - { - "type": "integer", - "default": 1, - "description": "页码", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "default": 50, - "description": "每页数量,别名 limit", - "name": "pageSize", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.TaskListResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/tasks/{taskID}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "返回指定任务的请求、状态、输出和执行摘要。", - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "获取任务详情", - "parameters": [ - { - "type": "string", - "description": "任务 ID", - "name": "taskID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/store.GatewayTask" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/tasks/{taskID}/cancel": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。", - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "取消异步任务", - "parameters": [ - { - "type": "string", - "description": "任务 ID", - "name": "taskID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.TaskCancelResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/tasks/{taskID}/events": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "以 text/event-stream 返回指定任务的历史事件;无事件时返回 task.accepted 占位事件。", - "produces": [ - "text/event-stream" - ], - "tags": [ - "tasks" - ], - "summary": "订阅任务事件", - "parameters": [ - { - "type": "string", - "description": "任务 ID", - "name": "taskID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Server-Sent Events,data 为 store.TaskEvent 或 TaskAcceptedEvent", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/tasks/{taskID}/param-preprocessing": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "返回指定任务在执行前的参数改写、校验或模板处理日志。", - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "获取任务参数预处理日志", - "parameters": [ - { - "type": "string", - "description": "任务 ID", - "name": "taskID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.TaskParamPreprocessingLogListResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/chat/completions": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "OpenAI-compatible Chat Completions 入口;仅接受官方字段及文档声明的 EasyAI 路由扩展,未知顶层字段返回 400 invalid_parameter。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "text/event-stream" - ], - "tags": [ - "chat" - ], - "summary": "创建 OpenAI Chat Completions", - "parameters": [ - { - "description": "Chat Completions 请求", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.ChatCompletionRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.ChatCompletionCompatibleResponse" - } - }, - "400": { - "description": "invalid_parameter", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/embeddings": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/files/upload": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "上传文件到配置的文件存储通道;没有启用通道时回退到本地静态上传目录。单文件最大 256MiB。", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "files" - ], - "summary": "上传文件", - "parameters": [ - { - "type": "file", - "description": "要上传的文件", - "name": "file", - "in": "formData", - "required": true - }, - { - "type": "string", - "default": "ai-gateway-openapi", - "description": "上传来源标识", - "name": "source", - "in": "formData" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.FileUploadResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/images/edits": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/images/generations": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/music/generations": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/reranks": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/responses": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "公开 OpenAI-compatible Responses 入口。模型声明 openai_responses 时原生转发,否则使用 Chat Completions 转换;store 缺省为 true。previous_response_id 严格绑定首次成功的平台模型和上游协议,链路不可用时不跨平台续接。未提供 previous_response_id 时由调用方管理完整状态,Gateway 以本轮 input/messages 为准且不追加本地历史。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "text/event-stream" - ], - "tags": [ - "responses" - ], - "summary": "创建 OpenAI Responses", - "parameters": [ - { - "description": "Responses 请求;Chat 回退只支持自定义 function tools", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.ResponsesRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.ResponsesCompatibleResponse" - }, - "headers": { - "X-Gateway-Task-Id": { - "type": "string", - "description": "网关审计任务 ID" - } - } - }, - "400": { - "description": "invalid_previous_response_id / unsupported_response_tool / unsupported_response_parameter", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "503": { - "description": "response_chain_unavailable", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/song/generations": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/speech/generations": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/tasks/{taskID}/cancel": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。", - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "取消异步任务", - "parameters": [ - { - "type": "string", - "description": "任务 ID", - "name": "taskID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.TaskCancelResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/videos/omni-video": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "兼容 Kling 旧版 /v1/videos/omni-video;Bearer token 必须为 Gateway API Key。任务固定异步执行,返回的 task_id 为网关任务 ID。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "kling-compatible" - ], - "summary": "创建 Kling Omni 视频任务", - "parameters": [ - { - "description": "Kling Omni 官方兼容请求", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.KelingOmniVideoRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - } - } - } - }, - "/v1/videos/omni-video/{taskID}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "按创建接口返回的网关 task_id 查询任务;仅允许创建任务的 Gateway 用户访问。", - "produces": [ - "application/json" - ], - "tags": [ - "kling-compatible" - ], - "summary": "查询 Kling Omni 视频任务", - "parameters": [ - { - "type": "string", - "description": "网关任务 ID", - "name": "taskID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.KelingCompatibleEnvelope" - } - } - } - } - }, - "/v1/voice_clone": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/voice_clone/voices": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。", - "produces": [ - "application/json" - ], - "tags": [ - "voice-clone" - ], - "summary": "列出当前用户克隆音色", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/v1/voice_clone/voices/{voiceID}": { - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。", - "produces": [ - "application/json" - ], - "tags": [ - "voice-clone" - ], - "summary": "删除当前用户克隆音色", - "parameters": [ - { - "type": "string", - "description": "克隆音色记录 ID 或上游 voice_id", - "name": "voiceID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/voice_clone": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "tasks" - ], - "summary": "创建或执行 AI 任务", - "parameters": [ - { - "type": "boolean", - "description": "true 时异步创建任务并返回 202", - "name": "X-Async", - "in": "header" - }, - { - "type": "string", - "description": "可选请求幂等键;同一用户范围内唯一", - "name": "Idempotency-Key", - "in": "header" - }, - { - "description": "AI 任务请求,字段随任务类型变化", - "name": "input", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/httpapi.TaskRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/httpapi.CompatibleResponse" - } - }, - "202": { - "description": "Accepted", - "schema": { - "$ref": "#/definitions/httpapi.TaskAcceptedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "402": { - "description": "Payment Required", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "429": { - "description": "Too Many Requests", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/voice_clone/voices": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。", - "produces": [ - "application/json" - ], - "tags": [ - "voice-clone" - ], - "summary": "列出当前用户克隆音色", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } - }, - "/voice_clone/voices/{voiceID}": { - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。", - "produces": [ - "application/json" - ], - "tags": [ - "voice-clone" - ], - "summary": "删除当前用户克隆音色", - "parameters": [ - { - "type": "string", - "description": "克隆音色记录 ID 或上游 voice_id", - "name": "voiceID", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": true - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "403": { - "description": "Forbidden", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/httpapi.ErrorEnvelope" - } - } - } - } } }, "definitions": { @@ -12461,11 +10389,11 @@ "properties": { "apiDocsJsonPath": { "type": "string", - "example": "/api-docs-json" + "example": "/api/v1/openapi.json" }, "apiDocsYamlPath": { "type": "string", - "example": "/api-docs-yaml" + "example": "/api/v1/openapi.yaml" }, "displayName": { "type": "string", @@ -13227,6 +11155,90 @@ "RevisionFailed" ] }, + "runner.PortraitAssetCapability": { + "type": "object", + "properties": { + "availablePlatformIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "canCreate": { + "type": "boolean" + }, + "canReferenceTencentAssetUri": { + "type": "boolean" + }, + "canUse": { + "type": "boolean" + }, + "canUseAsPlainMaterial": { + "type": "boolean" + }, + "canUseAsPortraitAsset": { + "type": "boolean" + }, + "creationPlatformIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "reason": { + "type": "string" + } + } + }, + "runner.PortraitAssetIssue": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + }, + "runner.PortraitAssetSyncResponse": { + "type": "object", + "properties": { + "accepted": { + "type": "integer" + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/store.PortraitAsset" + } + }, + "failed": { + "type": "array", + "items": { + "$ref": "#/definitions/runner.PortraitAssetIssue" + } + }, + "requested": { + "type": "integer" + }, + "skipped": { + "type": "array", + "items": { + "$ref": "#/definitions/runner.PortraitAssetIssue" + } + }, + "syncedIds": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "store.APIKey": { "type": "object", "properties": { @@ -15032,6 +13044,72 @@ } } }, + "store.PortraitAsset": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "gatewayTenantId": { + "type": "string" + }, + "gatewayUserId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastError": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": {} + }, + "mimeType": { + "type": "string" + }, + "name": { + "type": "string" + }, + "preview": { + "type": "string" + }, + "privateAvatarEligible": { + "type": "boolean" + }, + "size": { + "type": "integer" + }, + "sourceSha256": { + "type": "string" + }, + "sourceType": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "tenantKey": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + }, + "userId": { + "type": "string" + } + } + }, "store.PricingRule": { "type": "object", "properties": { diff --git a/apps/api/docs/swagger.yaml b/apps/api/docs/swagger.yaml index c378c00..211c814 100644 --- a/apps/api/docs/swagger.yaml +++ b/apps/api/docs/swagger.yaml @@ -939,10 +939,10 @@ definitions: httpapi.SkillBundleMetadataResponse: properties: apiDocsJsonPath: - example: /api-docs-json + example: /api/v1/openapi.json type: string apiDocsYamlPath: - example: /api-docs-yaml + example: /api/v1/openapi.yaml type: string displayName: example: AI Gateway 运维管理 @@ -1475,6 +1475,61 @@ definitions: - RevisionActive - RevisionSuperseded - RevisionFailed + runner.PortraitAssetCapability: + properties: + availablePlatformIds: + items: + type: string + type: array + canCreate: + type: boolean + canReferenceTencentAssetUri: + type: boolean + canUse: + type: boolean + canUseAsPlainMaterial: + type: boolean + canUseAsPortraitAsset: + type: boolean + creationPlatformIds: + items: + type: string + type: array + enabled: + type: boolean + reason: + type: string + type: object + runner.PortraitAssetIssue: + properties: + id: + type: string + reason: + type: string + type: object + runner.PortraitAssetSyncResponse: + properties: + accepted: + type: integer + assets: + items: + $ref: '#/definitions/store.PortraitAsset' + type: array + failed: + items: + $ref: '#/definitions/runner.PortraitAssetIssue' + type: array + requested: + type: integer + skipped: + items: + $ref: '#/definitions/runner.PortraitAssetIssue' + type: array + syncedIds: + items: + type: string + type: array + type: object store.APIKey: properties: createdAt: @@ -2695,6 +2750,50 @@ definitions: userId: type: string type: object + store.PortraitAsset: + properties: + createdAt: + type: string + description: + type: string + gatewayTenantId: + type: string + gatewayUserId: + type: string + id: + type: string + lastError: + type: string + metadata: + additionalProperties: {} + type: object + mimeType: + type: string + name: + type: string + preview: + type: string + privateAvatarEligible: + type: boolean + size: + type: integer + sourceSha256: + type: string + sourceType: + type: string + status: + type: string + tenantId: + type: string + tenantKey: + type: string + updatedAt: + type: string + url: + type: string + userId: + type: string + type: object store.PricingRule: properties: basePrice: @@ -3235,33 +3334,6 @@ info: title: EasyAI AI Gateway API version: 0.1.0 paths: - /api-docs-json: - get: - description: 返回当前构建内嵌的完整机器可读 Swagger JSON,供 Agent 在 SKILL references 未覆盖接口时查询。 - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - summary: 获取 AI Gateway Swagger JSON - tags: - - agent-resources - /api-docs-yaml: - get: - description: 返回当前构建内嵌的完整机器可读 Swagger YAML。 - produces: - - application/yaml - responses: - "200": - description: OK - schema: - type: string - summary: 获取 AI Gateway Swagger YAML - tags: - - agent-resources /api/admin/access-rules: get: description: 管理端返回用户组、租户、用户或 API Key 到平台、平台模型、基础模型的访问规则。 @@ -6270,6 +6342,21 @@ paths: summary: 列出 Playground API Key tags: - playground + /api/v1/ai/result/{taskID}: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 查询 server-main 兼容视频结果 + tags: + - volces-compatible /api/v1/api-keys: get: description: 返回当前用户创建的 API Key 元数据,secret 只在创建时返回。 @@ -6774,12 +6861,74 @@ paths: summary: 创建 Chat Completions tags: - tasks + /api/v1/contents/generations/tasks: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 列出火山内容生成任务 + tags: + - volces-compatible + post: + consumes: + - application/json + description: 统一公开入口兼容火山方舟内容生成任务。网关 task id 是查询与取消用的公开 id;上游 id 另以 upstream_task_id + 保留。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 创建火山内容生成任务 + tags: + - volces-compatible + /api/v1/contents/generations/tasks/{taskID}: + delete: + description: 取消网关任务;对于已提交且保存了上游任务标识的 Volces 视频任务,同时调用火山 DELETE 接口并持久化取消状态。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 取消火山内容生成任务 + tags: + - volces-compatible + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 查询火山内容生成任务 + tags: + - volces-compatible /api/v1/embeddings: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -6883,12 +7032,92 @@ paths: summary: 上传文件 tags: - files + /api/v1/gemini/upload/{version}/files: + post: + consumes: + - application/octet-stream + description: 使用统一 /api/v1 前缀启动或直接完成 Gemini Files 上传。 + parameters: + - description: Gemini 版本(v1 或 v1beta) + in: path + name: version + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: Gemini Files 上传接口 + tags: + - gemini-compatible + /api/v1/gemini/upload/{version}/files/{uploadID}: + post: + consumes: + - application/octet-stream + parameters: + - description: Gemini 版本(v1 或 v1beta) + in: path + name: version + required: true + type: string + - description: 上传会话 ID + in: path + name: uploadID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 完成 Gemini Files 分段上传 + tags: + - gemini-compatible + /api/v1/healthz: + get: + description: 返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.HealthResponse' + summary: 健康检查 + tags: + - system /api/v1/images/edits: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -6952,8 +7181,7 @@ paths: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -7013,6 +7241,181 @@ paths: summary: 创建或执行 AI 任务 tags: - tasks + /api/v1/kling/v1/videos/omni-video: + get: + parameters: + - default: 1 + description: 页码 + in: query + name: pageNum + type: integer + - default: 30 + description: 每页数量 + in: query + name: pageSize + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 分页查询可灵 V1 Omni 视频任务 + tags: + - kling-compatible + post: + consumes: + - application/json + description: 兼容中国区可灵 V1 /v1/videos/omni-video;用户使用网关 API Key,网关在服务端使用 AK/SK + 调用上游。 + parameters: + - description: 可灵 V1 Omni 请求 + in: body + name: input + required: true + schema: + additionalProperties: true + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 创建可灵 V1 Omni 视频任务 + tags: + - kling-compatible + /api/v1/kling/v1/videos/omni-video/{taskID}: + get: + parameters: + - description: 任务 ID + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "404": + description: Not Found + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 查询可灵 V1 Omni 视频任务 + tags: + - kling-compatible + /api/v1/kling/v2/omni-video/{model}: + post: + consumes: + - application/json + description: 兼容可灵 API 2.0 的模型路径;调用方使用网关 API Key,网关转换并使用中国区 V1 AK/SK 上游。 + parameters: + - description: 模型路径(kling-o1 或 kling-v3-omni) + in: path + name: model + required: true + type: string + - description: 可灵 API 2.0 请求 + in: body + name: input + required: true + schema: + additionalProperties: true + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: 创建可灵 API 2.0 Omni 视频任务 + tags: + - kling-compatible + /api/v1/kling/v2/tasks: + get: + parameters: + - description: 逗号分隔的任务 ID + in: query + name: task_ids + type: string + - description: 逗号分隔的外部任务 ID + in: query + name: external_task_ids + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 按 ID 查询可灵 API 2.0 任务 + tags: + - kling-compatible + post: + consumes: + - application/json + parameters: + - description: 游标、数量、时间范围和筛选条件 + in: body + name: input + required: true + schema: + additionalProperties: true + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 分页查询可灵 API 2.0 任务 + tags: + - kling-compatible /api/v1/me: get: description: 返回鉴权中解析出的用户、租户、用户组和 API Key 上下文。 @@ -7086,12 +7489,58 @@ paths: summary: 列出可调用模型 tags: - playground + /api/v1/models/{model}:generateContent: + post: + consumes: + - application/json + description: 使用统一 /api/v1 前缀接收 Gemini generateContent 请求;旧 /v1 和 /v1beta 路径保留兼容。 + parameters: + - description: 模型名称 + in: path + name: model + required: true + type: string + - description: Gemini generateContent 请求 + in: body + name: input + required: true + schema: + additionalProperties: true + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + security: + - BearerAuth: [] + summary: Gemini generateContent 兼容接口 + tags: + - gemini-compatible /api/v1/music/generations: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -7151,6 +7600,33 @@ paths: summary: 创建或执行 AI 任务 tags: - tasks + /api/v1/openapi.json: + get: + description: 返回当前构建内嵌的完整机器可读 Swagger JSON,供 Agent 在 SKILL references 未覆盖接口时查询。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + summary: 获取 AI Gateway Swagger JSON + tags: + - agent-resources + /api/v1/openapi.yaml: + get: + description: 返回当前构建内嵌的完整机器可读 Swagger YAML。 + produces: + - application/yaml + responses: + "200": + description: OK + schema: + type: string + summary: 获取 AI Gateway Swagger YAML + tags: + - agent-resources /api/v1/platforms: get: description: 按当前用户可访问模型过滤平台,仅返回启用且存在可访问模型的平台。 @@ -7347,12 +7823,28 @@ paths: summary: 获取 AI Gateway 运维管理 SKILL 元数据 tags: - agent-resources + /api/v1/readyz: + get: + description: 检查 Postgres 是否可用;数据库不可用时返回 503。 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.ReadyResponse' + "503": + description: Service Unavailable + schema: + $ref: '#/definitions/httpapi.ErrorEnvelope' + summary: 就绪检查 + tags: + - system /api/v1/reranks: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -7412,6 +7904,83 @@ paths: summary: 创建或执行 AI 任务 tags: - tasks + /api/v1/resource/material: + post: + consumes: + - multipart/form-data + description: 文件先写入网关文件存储;仅在 private_avatar_eligible=true 时登记到火山 Assets。创建后会立即触发一次状态同步。 + parameters: + - description: 真人资产源文件(图片、视频或音频) + in: formData + name: file + required: true + type: file + - description: material JSON,category 必须是 seedance_portrait_asset + in: formData + name: data + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 上传并创建 Seedance 真人资产 + tags: + - portrait-assets + /api/v1/resource/material/seedance-portrait-assets/capability: + get: + description: 返回当前网关是否已配置可创建、同步和引用的火山 Seedance 真人资产平台。 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/runner.PortraitAssetCapability' + security: + - BearerAuth: [] + summary: 查询 Seedance 真人资产能力 + tags: + - portrait-assets + /api/v1/resource/material/seedance-portrait-assets/sync: + post: + consumes: + - application/json + description: 调用火山 CreateAsset/GetAsset;多次调用可把 Processing 状态刷新为 Active 或 Failed。 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/runner.PortraitAssetSyncResponse' + security: + - BearerAuth: [] + summary: 同步 Seedance 真人资产状态 + tags: + - portrait-assets + /api/v1/resource/material/user/materials: + get: + description: 返回当前用户的真人资产;兼容 server-main material 列表响应字段。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 列出 Seedance 真人资产 + tags: + - portrait-assets /api/v1/responses: post: consumes: @@ -7519,8 +8088,7 @@ paths: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -7584,8 +8152,7 @@ paths: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -7843,12 +8410,29 @@ paths: summary: 获取任务参数预处理日志 tags: - tasks + /api/v1/video/generations: + post: + consumes: + - application/json + description: 兼容 server-main 的 /api/v1/video/generations,返回 submitted 和 task_id;额外保留火山任务字段。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + security: + - BearerAuth: [] + summary: 创建 server-main 兼容视频任务 + tags: + - volces-compatible /api/v1/videos/generations: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -7908,12 +8492,97 @@ paths: summary: 创建或执行 AI 任务 tags: - tasks + /api/v1/videos/omni-video: + post: + consumes: + - application/json + description: 兼容 Kling 旧版 /v1/videos/omni-video;Bearer token 必须为 Gateway API + Key。任务固定异步执行,返回的 task_id 为网关任务 ID。 + parameters: + - description: Kling Omni 官方兼容请求 + in: body + name: input + required: true + schema: + $ref: '#/definitions/httpapi.KelingOmniVideoRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "400": + description: Bad Request + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "503": + description: Service Unavailable + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + security: + - BearerAuth: [] + summary: 创建 Kling Omni 视频任务 + tags: + - kling-compatible + /api/v1/videos/omni-video/{taskID}: + get: + description: 按创建接口返回的网关 task_id 查询任务;仅允许创建任务的 Gateway 用户访问。 + parameters: + - description: 网关任务 ID + in: path + name: taskID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "403": + description: Forbidden + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "404": + description: Not Found + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' + security: + - BearerAuth: [] + summary: 查询 Kling Omni 视频任务 + tags: + - kling-compatible /api/v1/voice_clone: post: consumes: - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 + description: 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 parameters: - description: true 时异步创建任务并返回 202 in: header @@ -8390,768 +9059,6 @@ paths: summary: 列出钱包交易 tags: - wallet - /chat/completions: - post: - consumes: - - application/json - description: OpenAI-compatible Chat Completions 入口;仅接受官方字段及文档声明的 EasyAI 路由扩展,未知顶层字段返回 - 400 invalid_parameter。 - parameters: - - description: Chat Completions 请求 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.ChatCompletionRequest' - produces: - - application/json - - text/event-stream - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.ChatCompletionCompatibleResponse' - "400": - description: invalid_parameter - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建 OpenAI Chat Completions - tags: - - chat - /embeddings: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /healthz: - get: - description: 返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.HealthResponse' - summary: 健康检查 - tags: - - system - /images/edits: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /images/generations: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /kling/omni-video/{model}: - post: - consumes: - - application/json - description: 兼容可灵 API 2.0 的模型路径;调用方使用网关 API Key,网关转换并使用中国区 V1 AK/SK 上游。 - parameters: - - description: 模型路径(kling-o1 或 kling-v3-omni) - in: path - name: model - required: true - type: string - - description: 可灵 API 2.0 请求 - in: body - name: input - required: true - schema: - additionalProperties: true - type: object - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "400": - description: Bad Request - schema: - additionalProperties: true - type: object - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建可灵 API 2.0 Omni 视频任务 - tags: - - kling-compatible - /kling/tasks: - get: - parameters: - - description: 逗号分隔的任务 ID - in: query - name: task_ids - type: string - - description: 逗号分隔的外部任务 ID - in: query - name: external_task_ids - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - security: - - BearerAuth: [] - summary: 按 ID 查询可灵 API 2.0 任务 - tags: - - kling-compatible - post: - consumes: - - application/json - parameters: - - description: 游标、数量、时间范围和筛选条件 - in: body - name: input - required: true - schema: - additionalProperties: true - type: object - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - security: - - BearerAuth: [] - summary: 分页查询可灵 API 2.0 任务 - tags: - - kling-compatible - /kling/v1/videos/omni-video: - get: - parameters: - - default: 1 - description: 页码 - in: query - name: pageNum - type: integer - - default: 30 - description: 每页数量 - in: query - name: pageSize - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - security: - - BearerAuth: [] - summary: 分页查询可灵 V1 Omni 视频任务 - tags: - - kling-compatible - post: - consumes: - - application/json - description: 兼容中国区可灵 V1 /v1/videos/omni-video;用户使用网关 API Key,网关在服务端使用 AK/SK - 调用上游。 - parameters: - - description: 可灵 V1 Omni 请求 - in: body - name: input - required: true - schema: - additionalProperties: true - type: object - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "400": - description: Bad Request - schema: - additionalProperties: true - type: object - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建可灵 V1 Omni 视频任务 - tags: - - kling-compatible - /kling/v1/videos/omni-video/{taskID}: - get: - parameters: - - description: 任务 ID - in: path - name: taskID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "404": - description: Not Found - schema: - additionalProperties: true - type: object - security: - - BearerAuth: [] - summary: 查询可灵 V1 Omni 视频任务 - tags: - - kling-compatible - /music/generations: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /readyz: - get: - description: 检查 Postgres 是否可用;数据库不可用时返回 503。 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.ReadyResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - summary: 就绪检查 - tags: - - system - /reranks: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /responses: - post: - consumes: - - application/json - description: 公开 OpenAI-compatible Responses 入口。模型声明 openai_responses 时原生转发,否则使用 - Chat Completions 转换;store 缺省为 true。previous_response_id 严格绑定首次成功的平台模型和上游协议,链路不可用时不跨平台续接。未提供 - previous_response_id 时由调用方管理完整状态,Gateway 以本轮 input/messages 为准且不追加本地历史。 - parameters: - - description: Responses 请求;Chat 回退只支持自定义 function tools - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.ResponsesRequest' - produces: - - application/json - - text/event-stream - responses: - "200": - description: OK - headers: - X-Gateway-Task-Id: - description: 网关审计任务 ID - type: string - schema: - $ref: '#/definitions/httpapi.ResponsesCompatibleResponse' - "400": - description: invalid_previous_response_id / unsupported_response_tool / - unsupported_response_parameter - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "503": - description: response_chain_unavailable - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建 OpenAI Responses - tags: - - responses - /song/generations: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /speech/generations: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks /static/generated/{asset}: get: description: 从本地生成资源目录读取图片、视频等任务产物;不存在时返回 404。 @@ -9223,1190 +9130,6 @@ paths: summary: 获取本地上传资源 tags: - static - /tasks: - get: - description: 按当前用户列出任务,支持关键字、模型类型、时间范围和分页过滤。 - parameters: - - description: 搜索关键字,别名 query - in: query - name: q - type: string - - description: 模型类型,别名 type - in: query - name: modelType - type: string - - description: 创建时间起点,支持 RFC3339 或日期格式,别名 from - in: query - name: createdFrom - type: string - - description: 创建时间终点,支持 RFC3339 或日期格式,别名 to - in: query - name: createdTo - type: string - - default: 1 - description: 页码 - in: query - name: page - type: integer - - default: 50 - description: 每页数量,别名 limit - in: query - name: pageSize - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.TaskListResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 列出任务 - tags: - - tasks - /tasks/{taskID}: - get: - description: 返回指定任务的请求、状态、输出和执行摘要。 - parameters: - - description: 任务 ID - in: path - name: taskID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/store.GatewayTask' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 获取任务详情 - tags: - - tasks - /tasks/{taskID}/cancel: - post: - description: 仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。 - parameters: - - description: 任务 ID - in: path - name: taskID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.TaskCancelResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 取消异步任务 - tags: - - tasks - /tasks/{taskID}/events: - get: - description: 以 text/event-stream 返回指定任务的历史事件;无事件时返回 task.accepted 占位事件。 - parameters: - - description: 任务 ID - in: path - name: taskID - required: true - type: string - produces: - - text/event-stream - responses: - "200": - description: Server-Sent Events,data 为 store.TaskEvent 或 TaskAcceptedEvent - schema: - type: string - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 订阅任务事件 - tags: - - tasks - /tasks/{taskID}/param-preprocessing: - get: - description: 返回指定任务在执行前的参数改写、校验或模板处理日志。 - parameters: - - description: 任务 ID - in: path - name: taskID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.TaskParamPreprocessingLogListResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 获取任务参数预处理日志 - tags: - - tasks - /v1/chat/completions: - post: - consumes: - - application/json - description: OpenAI-compatible Chat Completions 入口;仅接受官方字段及文档声明的 EasyAI 路由扩展,未知顶层字段返回 - 400 invalid_parameter。 - parameters: - - description: Chat Completions 请求 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.ChatCompletionRequest' - produces: - - application/json - - text/event-stream - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.ChatCompletionCompatibleResponse' - "400": - description: invalid_parameter - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建 OpenAI Chat Completions - tags: - - chat - /v1/embeddings: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /v1/files/upload: - post: - consumes: - - multipart/form-data - description: 上传文件到配置的文件存储通道;没有启用通道时回退到本地静态上传目录。单文件最大 256MiB。 - parameters: - - description: 要上传的文件 - in: formData - name: file - required: true - type: file - - default: ai-gateway-openapi - description: 上传来源标识 - in: formData - name: source - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.FileUploadResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 上传文件 - tags: - - files - /v1/images/edits: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /v1/images/generations: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /v1/music/generations: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /v1/reranks: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /v1/responses: - post: - consumes: - - application/json - description: 公开 OpenAI-compatible Responses 入口。模型声明 openai_responses 时原生转发,否则使用 - Chat Completions 转换;store 缺省为 true。previous_response_id 严格绑定首次成功的平台模型和上游协议,链路不可用时不跨平台续接。未提供 - previous_response_id 时由调用方管理完整状态,Gateway 以本轮 input/messages 为准且不追加本地历史。 - parameters: - - description: Responses 请求;Chat 回退只支持自定义 function tools - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.ResponsesRequest' - produces: - - application/json - - text/event-stream - responses: - "200": - description: OK - headers: - X-Gateway-Task-Id: - description: 网关审计任务 ID - type: string - schema: - $ref: '#/definitions/httpapi.ResponsesCompatibleResponse' - "400": - description: invalid_previous_response_id / unsupported_response_tool / - unsupported_response_parameter - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "503": - description: response_chain_unavailable - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建 OpenAI Responses - tags: - - responses - /v1/song/generations: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /v1/speech/generations: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /v1/tasks/{taskID}/cancel: - post: - description: 仅取消仍在网关本地排队且尚未提交上游的任务;任务已运行、已提交上游或已结束时返回不可取消,客户端应继续查询结果。 - parameters: - - description: 任务 ID - in: path - name: taskID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.TaskCancelResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 取消异步任务 - tags: - - tasks - /v1/videos/omni-video: - post: - consumes: - - application/json - description: 兼容 Kling 旧版 /v1/videos/omni-video;Bearer token 必须为 Gateway API - Key。任务固定异步执行,返回的 task_id 为网关任务 ID。 - parameters: - - description: Kling Omni 官方兼容请求 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.KelingOmniVideoRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - security: - - BearerAuth: [] - summary: 创建 Kling Omni 视频任务 - tags: - - kling-compatible - /v1/videos/omni-video/{taskID}: - get: - description: 按创建接口返回的网关 task_id 查询任务;仅允许创建任务的 Gateway 用户访问。 - parameters: - - description: 网关任务 ID - in: path - name: taskID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.KelingCompatibleEnvelope' - security: - - BearerAuth: [] - summary: 查询 Kling Omni 视频任务 - tags: - - kling-compatible - /v1/voice_clone: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /v1/voice_clone/voices: - get: - description: 返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。 - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 列出当前用户克隆音色 - tags: - - voice-clone - /v1/voice_clone/voices/{voiceID}: - delete: - description: 先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。 - parameters: - - description: 克隆音色记录 ID 或上游 voice_id - in: path - name: voiceID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 删除当前用户克隆音色 - tags: - - voice-clone - /voice_clone: - post: - consumes: - - application/json - description: 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible - 路径同步返回兼容响应或 SSE 流。 - parameters: - - description: true 时异步创建任务并返回 202 - in: header - name: X-Async - type: boolean - - description: 可选请求幂等键;同一用户范围内唯一 - in: header - name: Idempotency-Key - type: string - - description: AI 任务请求,字段随任务类型变化 - in: body - name: input - required: true - schema: - $ref: '#/definitions/httpapi.TaskRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/httpapi.CompatibleResponse' - "202": - description: Accepted - schema: - $ref: '#/definitions/httpapi.TaskAcceptedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "402": - description: Payment Required - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "429": - description: Too Many Requests - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 创建或执行 AI 任务 - tags: - - tasks - /voice_clone/voices: - get: - description: 返回当前用户在网关中维护的克隆音色,以及克隆时绑定的平台与平台模型。 - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 列出当前用户克隆音色 - tags: - - voice-clone - /voice_clone/voices/{voiceID}: - delete: - description: 先从上游供应商删除音色,成功后再将网关数据库中的音色记录标记为 deleted。 - parameters: - - description: 克隆音色记录 ID 或上游 voice_id - in: path - name: voiceID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: true - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "403": - description: Forbidden - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "404": - description: Not Found - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/httpapi.ErrorEnvelope' - security: - - BearerAuth: [] - summary: 删除当前用户克隆音色 - tags: - - voice-clone schemes: - http - https diff --git a/apps/api/internal/httpapi/agent_resources_handlers.go b/apps/api/internal/httpapi/agent_resources_handlers.go index 9471763..3efd676 100644 --- a/apps/api/internal/httpapi/agent_resources_handlers.go +++ b/apps/api/internal/httpapi/agent_resources_handlers.go @@ -9,8 +9,8 @@ import ( const ( opsManagementSkillDownloadPath = "/api/v1/public/skills/ai-gateway-ops-management/download" - apiDocsJSONPath = "/api-docs-json" - apiDocsYAMLPath = "/api-docs-yaml" + apiDocsJSONPath = "/api/v1/openapi.json" + apiDocsYAMLPath = "/api/v1/openapi.yaml" ) // getOpsManagementSkillMetadata godoc @@ -64,7 +64,7 @@ func (s *Server) downloadOpsManagementSkill(w http.ResponseWriter, _ *http.Reque // @Tags agent-resources // @Produce json // @Success 200 {object} map[string]interface{} -// @Router /api-docs-json [get] +// @Router /api/v1/openapi.json [get] func (s *Server) apiDocsJSON(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) @@ -77,7 +77,7 @@ func (s *Server) apiDocsJSON(w http.ResponseWriter, _ *http.Request) { // @Tags agent-resources // @Produce application/yaml // @Success 200 {string} string -// @Router /api-docs-yaml [get] +// @Router /api/v1/openapi.yaml [get] func (s *Server) apiDocsYAML(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/yaml; charset=utf-8") w.WriteHeader(http.StatusOK) diff --git a/apps/api/internal/httpapi/agent_resources_handlers_test.go b/apps/api/internal/httpapi/agent_resources_handlers_test.go index f4426cb..5791f0e 100644 --- a/apps/api/internal/httpapi/agent_resources_handlers_test.go +++ b/apps/api/internal/httpapi/agent_resources_handlers_test.go @@ -32,7 +32,7 @@ func TestGetOpsManagementSkillMetadata(t *testing.T) { if len(metadata.Modules) != 1 || metadata.Modules[0] != "model-runtime" { t.Fatalf("unexpected metadata modules: %+v", metadata.Modules) } - if metadata.APIDocsJSONPath != "/api-docs-json" || metadata.APIDocsYAMLPath != "/api-docs-yaml" { + if metadata.APIDocsJSONPath != "/api/v1/openapi.json" || metadata.APIDocsYAMLPath != "/api/v1/openapi.yaml" { t.Fatalf("unexpected API docs paths: %+v", metadata) } } @@ -74,7 +74,7 @@ func TestEmbeddedAPIDocs(t *testing.T) { server := &Server{} jsonResponse := httptest.NewRecorder() - server.apiDocsJSON(jsonResponse, httptest.NewRequest(http.MethodGet, "/api-docs-json", nil)) + server.apiDocsJSON(jsonResponse, httptest.NewRequest(http.MethodGet, "/api/v1/openapi.json", nil)) if jsonResponse.Code != http.StatusOK { t.Fatalf("expected JSON docs status 200, got %d", jsonResponse.Code) } @@ -88,7 +88,7 @@ func TestEmbeddedAPIDocs(t *testing.T) { t.Fatalf("decode embedded Swagger JSON: %v", err) } for _, path := range []string{ - "/api-docs-json", + "/api/v1/openapi.json", "/api/v1/public/skills/ai-gateway-ops-management/download", "/api/admin/catalog/providers", "/api/admin/catalog/base-models", @@ -102,7 +102,7 @@ func TestEmbeddedAPIDocs(t *testing.T) { } yamlResponse := httptest.NewRecorder() - server.apiDocsYAML(yamlResponse, httptest.NewRequest(http.MethodGet, "/api-docs-yaml", nil)) + server.apiDocsYAML(yamlResponse, httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)) if yamlResponse.Code != http.StatusOK { t.Fatalf("expected YAML docs status 200, got %d", yamlResponse.Code) } diff --git a/apps/api/internal/httpapi/chat_completions_mode_test.go b/apps/api/internal/httpapi/chat_completions_mode_test.go index df744fe..9301155 100644 --- a/apps/api/internal/httpapi/chat_completions_mode_test.go +++ b/apps/api/internal/httpapi/chat_completions_mode_test.go @@ -58,17 +58,18 @@ func TestPlanTaskResponseTreatsAPIV1EmbeddingAndRerankAsSynchronousCompatibleRes } } -func TestPlanTaskResponseKeepsAsyncTaskModeForOtherAPIV1Tasks(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil) - req.Header.Set("X-Async", "true") - - plan := planTaskResponse("images.generations", false, map[string]any{"stream": true}, req) - - if !plan.asyncMode { - t.Fatal("non-chat /api/v1 task endpoints should keep X-Async task mode") +func TestPlanTaskResponseUsesCompatibleAPIV1MediaResponsesAndKeepsAsyncOptIn(t *testing.T) { + defaultRequest := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil) + defaultPlan := planTaskResponse("images.generations", true, map[string]any{}, defaultRequest) + if defaultPlan.asyncMode || !defaultPlan.compatibleMode { + t.Fatalf("canonical /api/v1 media endpoints should default to synchronous compatible responses, got %+v", defaultPlan) } - if plan.compatibleMode { - t.Fatal("non-compatible /api/v1 task endpoints should not return OpenAI-compatible payloads") + + asyncRequest := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil) + asyncRequest.Header.Set("X-Async", "true") + asyncPlan := planTaskResponse("images.generations", true, map[string]any{}, asyncRequest) + if !asyncPlan.asyncMode || !asyncPlan.compatibleMode { + t.Fatalf("canonical /api/v1 media endpoints should keep compatible mode when X-Async is enabled, got %+v", asyncPlan) } } diff --git a/apps/api/internal/httpapi/core_flow_integration_test.go b/apps/api/internal/httpapi/core_flow_integration_test.go index a20d169..59dc203 100644 --- a/apps/api/internal/httpapi/core_flow_integration_test.go +++ b/apps/api/internal/httpapi/core_flow_integration_test.go @@ -504,7 +504,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil { Result map[string]any `json:"result"` } `json:"task"` } - doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", apiKeyResponse.Secret, map[string]any{ + doJSONWithHeaders(t, server.URL, http.MethodPost, "/api/v1/images/generations", apiKeyResponse.Secret, map[string]any{ "model": defaultImageModel, "runMode": "simulation", "prompt": "a tiny gateway console", @@ -512,7 +512,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil { "quality": "medium", "simulation": true, "simulationDurationMs": 5, - }, http.StatusAccepted, &imageResponse) + }, map[string]string{"X-Async": "true"}, http.StatusAccepted, &imageResponse) if imageResponse.Task.Status != "succeeded" || imageResponse.Task.Result["id"] == "" { t.Fatalf("unexpected image generation task: %+v", imageResponse.Task) } @@ -524,7 +524,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil { Result map[string]any `json:"result"` } `json:"task"` } - doJSON(t, server.URL, http.MethodPost, "/api/v1/images/edits", apiKeyResponse.Secret, map[string]any{ + doJSONWithHeaders(t, server.URL, http.MethodPost, "/api/v1/images/edits", apiKeyResponse.Secret, map[string]any{ "model": defaultImageModel, "runMode": "simulation", "prompt": "replace background with clean studio light", @@ -532,7 +532,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil { "mask": "https://example.com/mask.png", "simulation": true, "simulationDurationMs": 5, - }, http.StatusAccepted, &imageEditResponse) + }, map[string]string{"X-Async": "true"}, http.StatusAccepted, &imageEditResponse) if imageEditResponse.Task.Status != "succeeded" || imageEditResponse.Task.Result["id"] == "" { t.Fatalf("unexpected image edit task: %+v", imageEditResponse.Task) } @@ -1200,13 +1200,13 @@ WHERE reference_type = 'gateway_task' ModelType string `json:"modelType"` } `json:"task"` } - doJSON(t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, map[string]any{ + doJSONWithHeaders(t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, map[string]any{ "model": videoRouteModel, "runMode": "simulation", "simulation": true, "simulationDurationMs": 5, "prompt": "text to video route", - }, http.StatusAccepted, &textToVideoTask) + }, map[string]string{"X-Async": "true"}, http.StatusAccepted, &textToVideoTask) if textToVideoTask.Task.Status != "succeeded" || textToVideoTask.Task.ModelType != "video_generate" { t.Fatalf("text-to-video request should use video_generate model_type: %+v", textToVideoTask.Task) } @@ -1218,14 +1218,14 @@ WHERE reference_type = 'gateway_task' Metrics map[string]any `json:"metrics"` } `json:"task"` } - doJSON(t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, map[string]any{ + doJSONWithHeaders(t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, map[string]any{ "model": videoRouteModel, "runMode": "simulation", "simulation": true, "simulationDurationMs": 5, "prompt": "image to video route", "image": "https://example.com/source.png", - }, http.StatusAccepted, &imageToVideoTask) + }, map[string]string{"X-Async": "true"}, http.StatusAccepted, &imageToVideoTask) if imageToVideoTask.Task.Status != "succeeded" || imageToVideoTask.Task.ModelType != "image_to_video" { t.Fatalf("image-to-video request should use image_to_video model_type: %+v", imageToVideoTask.Task) } diff --git a/apps/api/internal/httpapi/file_upload_handlers.go b/apps/api/internal/httpapi/file_upload_handlers.go index ee807e6..07f49ef 100644 --- a/apps/api/internal/httpapi/file_upload_handlers.go +++ b/apps/api/internal/httpapi/file_upload_handlers.go @@ -26,7 +26,6 @@ const maxGatewayUploadBytes = 256 << 20 // @Failure 502 {object} ErrorEnvelope // @Failure 503 {object} ErrorEnvelope // @Router /api/v1/files/upload [post] -// @Router /v1/files/upload [post] func (s *Server) uploadFile(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxGatewayUploadBytes) if err := r.ParseMultipartForm(32 << 20); err != nil { diff --git a/apps/api/internal/httpapi/gemini_compat.go b/apps/api/internal/httpapi/gemini_compat.go index 3e3b93e..bd0e8f7 100644 --- a/apps/api/internal/httpapi/gemini_compat.go +++ b/apps/api/internal/httpapi/gemini_compat.go @@ -75,6 +75,21 @@ func geminiGenerateContentModelFromPath(prefix string, requestPath string) (stri return model, true } +// geminiGenerateContent godoc +// @Summary Gemini generateContent 兼容接口 +// @Description 使用统一 /api/v1 前缀接收 Gemini generateContent 请求;旧 /v1 和 /v1beta 路径保留兼容。 +// @Tags gemini-compatible +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param model path string true "模型名称" +// @Param input body map[string]interface{} true "Gemini generateContent 请求" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} ErrorEnvelope +// @Failure 401 {object} ErrorEnvelope +// @Failure 403 {object} ErrorEnvelope +// @Failure 502 {object} ErrorEnvelope +// @Router /api/v1/models/{model}:generateContent [post] func (s *Server) geminiGenerateContent(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok { @@ -395,6 +410,18 @@ func geminiUsageMetadataFromOutput(output map[string]any) map[string]any { return meta } +// geminiFilesUpload godoc +// @Summary Gemini Files 上传接口 +// @Description 使用统一 /api/v1 前缀启动或直接完成 Gemini Files 上传。 +// @Tags gemini-compatible +// @Accept octet-stream +// @Produce json +// @Security BearerAuth +// @Param version path string true "Gemini 版本(v1 或 v1beta)" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} ErrorEnvelope +// @Failure 401 {object} ErrorEnvelope +// @Router /api/v1/gemini/upload/{version}/files [post] func (s *Server) geminiFilesUpload(w http.ResponseWriter, r *http.Request) { if err := validateGeminiFilesVersion(r.PathValue("version")); err != nil { writeError(w, http.StatusBadRequest, err.Error(), clients.ErrorCode(err)) @@ -412,6 +439,18 @@ func (s *Server) geminiFilesUpload(w http.ResponseWriter, r *http.Request) { }) } +// geminiFilesUploadFinalize godoc +// @Summary 完成 Gemini Files 分段上传 +// @Tags gemini-compatible +// @Accept octet-stream +// @Produce json +// @Security BearerAuth +// @Param version path string true "Gemini 版本(v1 或 v1beta)" +// @Param uploadID path string true "上传会话 ID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} ErrorEnvelope +// @Failure 401 {object} ErrorEnvelope +// @Router /api/v1/gemini/upload/{version}/files/{uploadID} [post] func (s *Server) geminiFilesUploadFinalize(w http.ResponseWriter, r *http.Request) { if err := validateGeminiFilesVersion(r.PathValue("version")); err != nil { writeError(w, http.StatusBadRequest, err.Error(), clients.ErrorCode(err)) @@ -440,11 +479,18 @@ func (s *Server) startGeminiFilesUpload(w http.ResponseWriter, r *http.Request) CreatedAt: time.Now(), } s.geminiUploadSessions.Store(uploadID, session) - w.Header().Set("X-Goog-Upload-URL", absoluteRequestURL(r, "/upload/"+session.Version+"/files/"+uploadID)) + w.Header().Set("X-Goog-Upload-URL", absoluteRequestURL(r, geminiUploadPath(r, session.Version, uploadID))) w.Header().Set("X-Goog-Upload-Status", "active") writeJSON(w, http.StatusOK, map[string]any{}) } +func geminiUploadPath(r *http.Request, version string, uploadID string) string { + if strings.HasPrefix(r.URL.Path, "/api/v1/gemini/upload/") { + return "/api/v1/gemini/upload/" + version + "/files/" + uploadID + } + return "/upload/" + version + "/files/" + uploadID +} + func (s *Server) finalizeGeminiFilesUpload(w http.ResponseWriter, r *http.Request, uploadID string, session geminiUploadSession) { if uploadID == "" { uploadID = newGeminiUploadID() diff --git a/apps/api/internal/httpapi/gemini_compat_test.go b/apps/api/internal/httpapi/gemini_compat_test.go index fcf20ec..ebd1a66 100644 --- a/apps/api/internal/httpapi/gemini_compat_test.go +++ b/apps/api/internal/httpapi/gemini_compat_test.go @@ -102,6 +102,18 @@ func TestRegisterGeminiGenerateContentRoutes(t *testing.T) { } } +func TestGeminiUploadPathKeepsCanonicalV1Prefix(t *testing.T) { + canonical := httptest.NewRequest(http.MethodPost, "/api/v1/gemini/upload/v1beta/files", nil) + if got := geminiUploadPath(canonical, "v1beta", "upload-1"); got != "/api/v1/gemini/upload/v1beta/files/upload-1" { + t.Fatalf("canonical upload path = %q", got) + } + + legacy := httptest.NewRequest(http.MethodPost, "/upload/v1beta/files", nil) + if got := geminiUploadPath(legacy, "v1beta", "upload-1"); got != "/upload/v1beta/files/upload-1" { + t.Fatalf("legacy upload path = %q", got) + } +} + func TestGeminiImageTaskBodyMapsTextOnlyToImageGenerate(t *testing.T) { mapping, err := geminiImageTaskBody("gemini-image", map[string]any{ "contents": []any{ diff --git a/apps/api/internal/httpapi/handlers.go b/apps/api/internal/httpapi/handlers.go index 0e29b08..5ef7fb9 100644 --- a/apps/api/internal/httpapi/handlers.go +++ b/apps/api/internal/httpapi/handlers.go @@ -31,7 +31,7 @@ const ( // @Tags system // @Produce json // @Success 200 {object} HealthResponse -// @Router /healthz [get] +// @Router /api/v1/healthz [get] func (s *Server) health(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "ok": true, @@ -48,7 +48,7 @@ func (s *Server) health(w http.ResponseWriter, r *http.Request) { // @Produce json // @Success 200 {object} ReadyResponse // @Failure 503 {object} ErrorEnvelope -// @Router /readyz [get] +// @Router /api/v1/readyz [get] func (s *Server) ready(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), postgresReadinessTimeout) defer cancel() @@ -1021,7 +1021,7 @@ func (s *Server) listModelRateLimitStatuses(w http.ResponseWriter, r *http.Reque // createTask godoc // @Summary 创建或执行 AI 任务 -// @Description 网关任务接口按 model 选择平台模型;除 /api/v1/chat/completions 以外的 /api/v1 任务路径返回任务受理结果,OpenAI-compatible 路径同步返回兼容响应或 SSE 流。 +// @Description 统一公开入口按 model 选择平台模型并默认同步返回兼容响应;设置 X-Async=true 时异步创建任务并返回 202。 // @Tags tasks // @Accept json // @Produce json @@ -1047,22 +1047,6 @@ func (s *Server) listModelRateLimitStatuses(w http.ResponseWriter, r *http.Reque // @Router /api/v1/music/generations [post] // @Router /api/v1/speech/generations [post] // @Router /api/v1/voice_clone [post] -// @Router /embeddings [post] -// @Router /v1/embeddings [post] -// @Router /reranks [post] -// @Router /v1/reranks [post] -// @Router /images/generations [post] -// @Router /v1/images/generations [post] -// @Router /images/edits [post] -// @Router /v1/images/edits [post] -// @Router /song/generations [post] -// @Router /v1/song/generations [post] -// @Router /music/generations [post] -// @Router /v1/music/generations [post] -// @Router /speech/generations [post] -// @Router /v1/speech/generations [post] -// @Router /voice_clone [post] -// @Router /v1/voice_clone [post] func (s *Server) createTask(kind string, compatible bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) @@ -1214,8 +1198,6 @@ func (s *Server) createAPIV1ChatCompletions() http.Handler { // @Failure 403 {object} ErrorEnvelope // @Failure 429 {object} ErrorEnvelope // @Failure 502 {object} ErrorEnvelope -// @Router /chat/completions [post] -// @Router /v1/chat/completions [post] func openAIChatCompletionsDoc() {} // openAIResponsesDoc godoc @@ -1234,8 +1216,6 @@ func openAIChatCompletionsDoc() {} // @Failure 402 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 503 {object} ErrorEnvelope "response_chain_unavailable" -// @Router /responses [post] -// @Router /v1/responses [post] // @Router /api/v1/responses [post] func openAIResponsesDoc() {} @@ -1660,7 +1640,6 @@ func matchedRateLimitRule(policy map[string]any, metric string) map[string]any { // @Failure 500 {object} ErrorEnvelope // @Router /api/workspace/tasks [get] // @Router /api/v1/tasks [get] -// @Router /tasks [get] func (s *Server) listTasks(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok { @@ -1769,7 +1748,6 @@ func boolValue(body map[string]any, key string) bool { // @Failure 500 {object} ErrorEnvelope // @Router /api/workspace/tasks/{taskID} [get] // @Router /api/v1/tasks/{taskID} [get] -// @Router /tasks/{taskID} [get] func (s *Server) getTask(w http.ResponseWriter, r *http.Request) { task, err := s.store.GetTask(r.Context(), r.PathValue("taskID")) if err == nil { @@ -1802,8 +1780,6 @@ func (s *Server) getTask(w http.ResponseWriter, r *http.Request) { // @Failure 500 {object} ErrorEnvelope // @Router /api/workspace/tasks/{taskID}/cancel [post] // @Router /api/v1/tasks/{taskID}/cancel [post] -// @Router /v1/tasks/{taskID}/cancel [post] -// @Router /tasks/{taskID}/cancel [post] func (s *Server) cancelTask(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok { @@ -1840,7 +1816,6 @@ func (s *Server) cancelTask(w http.ResponseWriter, r *http.Request) { // @Failure 500 {object} ErrorEnvelope // @Router /api/workspace/tasks/{taskID}/param-preprocessing [get] // @Router /api/v1/tasks/{taskID}/param-preprocessing [get] -// @Router /tasks/{taskID}/param-preprocessing [get] func (s *Server) taskParamPreprocessing(w http.ResponseWriter, r *http.Request) { task, err := s.store.GetTask(r.Context(), r.PathValue("taskID")) if err != nil { @@ -1874,7 +1849,6 @@ func (s *Server) taskParamPreprocessing(w http.ResponseWriter, r *http.Request) // @Failure 500 {object} ErrorEnvelope // @Router /api/workspace/tasks/{taskID}/events [get] // @Router /api/v1/tasks/{taskID}/events [get] -// @Router /tasks/{taskID}/events [get] func (s *Server) taskEvents(w http.ResponseWriter, r *http.Request) { task, err := s.store.GetTask(r.Context(), r.PathValue("taskID")) if err != nil { diff --git a/apps/api/internal/httpapi/keling_compat_handlers.go b/apps/api/internal/httpapi/keling_compat_handlers.go index 56151ad..eb1ef30 100644 --- a/apps/api/internal/httpapi/keling_compat_handlers.go +++ b/apps/api/internal/httpapi/keling_compat_handlers.go @@ -132,7 +132,7 @@ func (s *Server) requireKelingAPIKey(next http.Handler) http.Handler { // @Failure 429 {object} KelingCompatibleEnvelope // @Failure 500 {object} KelingCompatibleEnvelope // @Failure 503 {object} KelingCompatibleEnvelope -// @Router /v1/videos/omni-video [post] +// @Router /api/v1/videos/omni-video [post] func (s *Server) createKelingOmniVideo(w http.ResponseWriter, r *http.Request) { requestID := kelingCompatRequestID(r) user, ok := auth.UserFromContext(r.Context()) @@ -206,7 +206,7 @@ func (s *Server) createKelingOmniVideo(w http.ResponseWriter, r *http.Request) { // @Failure 403 {object} KelingCompatibleEnvelope // @Failure 404 {object} KelingCompatibleEnvelope // @Failure 500 {object} KelingCompatibleEnvelope -// @Router /v1/videos/omni-video/{taskID} [get] +// @Router /api/v1/videos/omni-video/{taskID} [get] func (s *Server) getKelingOmniVideo(w http.ResponseWriter, r *http.Request) { requestID := kelingCompatRequestID(r) user, ok := auth.UserFromContext(r.Context()) diff --git a/apps/api/internal/httpapi/keling_compat_integration_test.go b/apps/api/internal/httpapi/keling_compat_integration_test.go index c48b85c..89ac4fe 100644 --- a/apps/api/internal/httpapi/keling_compat_integration_test.go +++ b/apps/api/internal/httpapi/keling_compat_integration_test.go @@ -145,7 +145,7 @@ func TestKelingOmniCompatibleHTTPFlow(t *testing.T) { _, secondAPIKey := createKelingCompatIntegrationUser(t, ctx, db, gateway.URL, "second", suffix, false) var created KelingCompatibleEnvelope - doJSON(t, gateway.URL, http.MethodPost, "/v1/videos/omni-video", firstAPIKey, map[string]any{ + doJSON(t, gateway.URL, http.MethodPost, "/api/v1/videos/omni-video", firstAPIKey, map[string]any{ "model_name": "kling-video-o1", "prompt": "A clean product reveal", "mode": "std", @@ -162,7 +162,7 @@ func TestKelingOmniCompatibleHTTPFlow(t *testing.T) { taskID := stringFromKelingCompat(createdData["task_id"]) var hidden KelingCompatibleEnvelope - doJSON(t, gateway.URL, http.MethodGet, "/v1/videos/omni-video/"+taskID, secondAPIKey, nil, http.StatusNotFound, &hidden) + doJSON(t, gateway.URL, http.MethodGet, "/api/v1/videos/omni-video/"+taskID, secondAPIKey, nil, http.StatusNotFound, &hidden) if hidden.Code != 1203 { t.Fatalf("cross-user task must be hidden: %+v", hidden) } @@ -278,7 +278,7 @@ func waitForKelingCompatTask(t *testing.T, baseURL string, apiKey string, taskID deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { var response KelingCompatibleEnvelope - doJSON(t, baseURL, http.MethodGet, "/v1/videos/omni-video/"+taskID, apiKey, nil, http.StatusOK, &response) + doJSON(t, baseURL, http.MethodGet, "/api/v1/videos/omni-video/"+taskID, apiKey, nil, http.StatusOK, &response) data, _ := response.Data.(map[string]any) switch data["task_status"] { case "succeed": diff --git a/apps/api/internal/httpapi/keling_simulation_integration_test.go b/apps/api/internal/httpapi/keling_simulation_integration_test.go index 421ce32..ecdca2d 100644 --- a/apps/api/internal/httpapi/keling_simulation_integration_test.go +++ b/apps/api/internal/httpapi/keling_simulation_integration_test.go @@ -134,13 +134,14 @@ WHERE username = $1`, username); err != nil { ResponseDurationMS int64 `json:"responseDurationMs"` } `json:"task"` } - doJSON( + doJSONWithHeaders( t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, request, + map[string]string{"X-Async": "true"}, http.StatusAccepted, &response, ) diff --git a/apps/api/internal/httpapi/kling_compat.go b/apps/api/internal/httpapi/kling_compat.go index 79636a3..fc49613 100644 --- a/apps/api/internal/httpapi/kling_compat.go +++ b/apps/api/internal/httpapi/kling_compat.go @@ -27,6 +27,15 @@ func (s *Server) registerKlingCompatibilityRoutes(mux *http.ServeMux) { handler := func(next http.HandlerFunc) http.Handler { return s.requireUser(auth.PermissionBasic, http.HandlerFunc(next)) } + // /api/v1 is the canonical public prefix. The historical /kling paths + // remain registered below so existing clients can migrate without downtime. + mux.Handle("POST /api/v1/kling/v1/videos/omni-video", handler(s.klingV1CreateOmniVideo)) + mux.Handle("GET /api/v1/kling/v1/videos/omni-video", handler(s.klingV1ListOmniVideos)) + mux.Handle("GET /api/v1/kling/v1/videos/omni-video/{taskID}", handler(s.klingV1GetOmniVideo)) + mux.Handle("POST /api/v1/kling/v2/omni-video/{model}", handler(s.klingV2CreateOmniVideo)) + mux.Handle("GET /api/v1/kling/v2/tasks", handler(s.klingV2GetTasks)) + mux.Handle("POST /api/v1/kling/v2/tasks", handler(s.klingV2ListTasks)) + mux.Handle("POST /kling/v1/videos/omni-video", handler(s.klingV1CreateOmniVideo)) mux.Handle("GET /kling/v1/videos/omni-video", handler(s.klingV1ListOmniVideos)) mux.Handle("GET /kling/v1/videos/omni-video/{taskID}", handler(s.klingV1GetOmniVideo)) @@ -52,7 +61,7 @@ func (s *Server) registerKlingCompatibilityRoutes(mux *http.ServeMux) { // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]interface{} // @Failure 401 {object} ErrorEnvelope -// @Router /kling/v1/videos/omni-video [post] +// @Router /api/v1/kling/v1/videos/omni-video [post] func (s *Server) klingV1CreateOmniVideo(w http.ResponseWriter, r *http.Request) { var native map[string]any if err := decodeKlingJSON(r, &native); err != nil { @@ -78,7 +87,7 @@ func (s *Server) klingV1CreateOmniVideo(w http.ResponseWriter, r *http.Request) // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]interface{} // @Failure 401 {object} ErrorEnvelope -// @Router /kling/omni-video/{model} [post] +// @Router /api/v1/kling/v2/omni-video/{model} [post] func (s *Server) klingV2CreateOmniVideo(w http.ResponseWriter, r *http.Request) { model, ok := klingV2ProviderModel(r.PathValue("model")) if !ok { @@ -433,7 +442,7 @@ func validateKlingCompatBody(model string, body map[string]any) error { // @Param taskID path string true "任务 ID" // @Success 200 {object} map[string]interface{} // @Failure 404 {object} map[string]interface{} -// @Router /kling/v1/videos/omni-video/{taskID} [get] +// @Router /api/v1/kling/v1/videos/omni-video/{taskID} [get] func (s *Server) klingV1GetOmniVideo(w http.ResponseWriter, r *http.Request) { user, _ := auth.UserFromContext(r.Context()) task, err := s.store.GetCompatTask(r.Context(), user, klingCompatProvider, "v1", r.PathValue("taskID")) @@ -456,7 +465,7 @@ func (s *Server) klingV1GetOmniVideo(w http.ResponseWriter, r *http.Request) { // @Param pageNum query int false "页码" default(1) // @Param pageSize query int false "每页数量" default(30) // @Success 200 {object} map[string]interface{} -// @Router /kling/v1/videos/omni-video [get] +// @Router /api/v1/kling/v1/videos/omni-video [get] func (s *Server) klingV1ListOmniVideos(w http.ResponseWriter, r *http.Request) { user, _ := auth.UserFromContext(r.Context()) page, err := positiveQueryInt(r.URL.Query().Get("pageNum"), 1) @@ -489,7 +498,7 @@ func (s *Server) klingV1ListOmniVideos(w http.ResponseWriter, r *http.Request) { // @Param task_ids query string false "逗号分隔的任务 ID" // @Param external_task_ids query string false "逗号分隔的外部任务 ID" // @Success 200 {object} map[string]interface{} -// @Router /kling/tasks [get] +// @Router /api/v1/kling/v2/tasks [get] func (s *Server) klingV2GetTasks(w http.ResponseWriter, r *http.Request) { user, _ := auth.UserFromContext(r.Context()) taskIDs := splitKlingIDs(r.URL.Query().Get("task_ids")) @@ -525,7 +534,7 @@ func (s *Server) klingV2GetTasks(w http.ResponseWriter, r *http.Request) { // @Security BearerAuth // @Param input body map[string]interface{} true "游标、数量、时间范围和筛选条件" // @Success 200 {object} map[string]interface{} -// @Router /kling/tasks [post] +// @Router /api/v1/kling/v2/tasks [post] func (s *Server) klingV2ListTasks(w http.ResponseWriter, r *http.Request) { user, _ := auth.UserFromContext(r.Context()) var body map[string]any diff --git a/apps/api/internal/httpapi/kling_compat_integration_test.go b/apps/api/internal/httpapi/kling_compat_integration_test.go index 5801f37..15cc410 100644 --- a/apps/api/internal/httpapi/kling_compat_integration_test.go +++ b/apps/api/internal/httpapi/kling_compat_integration_test.go @@ -143,7 +143,7 @@ WHERE platform_id = $1::uuid ResolvedModel string `json:"resolvedModel"` } `json:"task"` } - doJSON(t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, request, http.StatusAccepted, &response) + doJSONWithHeaders(t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, request, map[string]string{"X-Async": "true"}, http.StatusAccepted, &response) resolvedModel, resolved := klingV2ProviderModel(response.Task.ResolvedModel) if response.Task.Status != "succeeded" || response.Task.ModelType != expectedModelType || !resolved || resolvedModel != model { t.Fatalf("generic video request without modelType should use inferred capability: %+v", response.Task) @@ -159,7 +159,7 @@ WHERE platform_id = $1::uuid createV1 := func(model string, duration int, externalID string) string { t.Helper() var response map[string]any - doJSON(t, server.URL, http.MethodPost, "/kling/v1/videos/omni-video", apiKeyResponse.Secret, map[string]any{ + doJSON(t, server.URL, http.MethodPost, "/api/v1/kling/v1/videos/omni-video", apiKeyResponse.Secret, map[string]any{ "model_name": model, "prompt": "兼容接口模拟任务", "duration": duration, @@ -186,14 +186,14 @@ WHERE platform_id = $1::uuid waitKlingV1SimulationTask(t, server.URL, apiKeyResponse.Secret, taskID) } var listResponse map[string]any - doJSON(t, server.URL, http.MethodGet, "/kling/v1/videos/omni-video?pageNum=1&pageSize=10", apiKeyResponse.Secret, nil, http.StatusOK, &listResponse) + doJSON(t, server.URL, http.MethodGet, "/api/v1/kling/v1/videos/omni-video?pageNum=1&pageSize=10", apiKeyResponse.Secret, nil, http.StatusOK, &listResponse) items, _ := listResponse["data"].([]any) if len(items) < 2 { t.Fatalf("V1 task list did not return compatibility tasks: %#v", listResponse) } var duplicateResponse map[string]any - doJSON(t, server.URL, http.MethodPost, "/kling/v1/videos/omni-video", apiKeyResponse.Secret, map[string]any{ + doJSON(t, server.URL, http.MethodPost, "/api/v1/kling/v1/videos/omni-video", apiKeyResponse.Secret, map[string]any{ "model_name": klingO1Model, "prompt": "duplicate", "duration": 5, "external_task_id": "compat-o1-" + suffix, "runMode": "simulation", "simulation": true, @@ -203,7 +203,7 @@ WHERE platform_id = $1::uuid } var v2Response map[string]any - doJSON(t, server.URL, http.MethodPost, "/kling/v2/omni-video/kling-v3-omni", apiKeyResponse.Secret, map[string]any{ + doJSON(t, server.URL, http.MethodPost, "/api/v1/kling/v2/omni-video/kling-v3-omni", apiKeyResponse.Secret, map[string]any{ "contents": []any{map[string]any{"type": "prompt", "text": "API 2.0 模拟任务"}}, "settings": map[string]any{"duration": 3, "resolution": "720p", "aspect_ratio": "16:9", "audio": "off"}, "options": map[string]any{"external_task_id": "compat-v2-" + suffix}, @@ -224,7 +224,7 @@ func waitKlingV1SimulationTask(t *testing.T, baseURL string, apiKey string, task deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { var response map[string]any - doJSON(t, baseURL, http.MethodGet, "/kling/v1/videos/omni-video/"+taskID, apiKey, nil, http.StatusOK, &response) + doJSON(t, baseURL, http.MethodGet, "/api/v1/kling/v1/videos/omni-video/"+taskID, apiKey, nil, http.StatusOK, &response) data, _ := response["data"].(map[string]any) switch data["task_status"] { case "succeed": @@ -242,7 +242,7 @@ func waitKlingV2SimulationTask(t *testing.T, baseURL string, apiKey string, task deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { var response map[string]any - doJSON(t, baseURL, http.MethodGet, "/kling/v2/tasks?task_ids="+taskID, apiKey, nil, http.StatusOK, &response) + doJSON(t, baseURL, http.MethodGet, "/api/v1/kling/v2/tasks?task_ids="+taskID, apiKey, nil, http.StatusOK, &response) items, _ := response["data"].([]any) if len(items) == 1 { data, _ := items[0].(map[string]any) diff --git a/apps/api/internal/httpapi/openapi_models.go b/apps/api/internal/httpapi/openapi_models.go index 918ce20..40cd84a 100644 --- a/apps/api/internal/httpapi/openapi_models.go +++ b/apps/api/internal/httpapi/openapi_models.go @@ -23,8 +23,8 @@ type SkillBundleMetadataResponse struct { Modules []string `json:"modules" example:"model-runtime"` FileName string `json:"fileName" example:"ai-gateway-ops-management-v1.0.2.zip"` DownloadPath string `json:"downloadPath" example:"/api/v1/public/skills/ai-gateway-ops-management/download"` - APIDocsJSONPath string `json:"apiDocsJsonPath" example:"/api-docs-json"` - APIDocsYAMLPath string `json:"apiDocsYamlPath" example:"/api-docs-yaml"` + APIDocsJSONPath string `json:"apiDocsJsonPath" example:"/api/v1/openapi.json"` + APIDocsYAMLPath string `json:"apiDocsYamlPath" example:"/api/v1/openapi.yaml"` } type ErrorEnvelope struct { diff --git a/apps/api/internal/httpapi/public_api_prefix_test.go b/apps/api/internal/httpapi/public_api_prefix_test.go new file mode 100644 index 0000000..1ef24a5 --- /dev/null +++ b/apps/api/internal/httpapi/public_api_prefix_test.go @@ -0,0 +1,72 @@ +package httpapi + +import ( + "encoding/json" + "strings" + "testing" + + gatewaydocs "github.com/easyai/easyai-ai-gateway/apps/api/docs" +) + +func TestOpenAPIPublicRoutesUseCanonicalV1Prefix(t *testing.T) { + var document struct { + Paths map[string]any `json:"paths"` + } + if err := json.Unmarshal(gatewaydocs.SwaggerJSON, &document); err != nil { + t.Fatalf("decode embedded OpenAPI document: %v", err) + } + + legacyPrefixes := []string{ + "/v1/", + "/v1beta/", + "/kling/", + "/upload/", + "/api/v3/", + "/chat/", + "/images/", + "/song/", + "/music/", + "/speech/", + "/voice_clone", + "/tasks", + } + legacyExact := map[string]bool{ + "/healthz": true, + "/readyz": true, + "/api-docs-json": true, + "/api-docs-yaml": true, + "/responses": true, + "/embeddings": true, + "/reranks": true, + } + for route := range document.Paths { + if legacyExact[route] { + t.Errorf("legacy public route must not be advertised in OpenAPI: %s", route) + } + for _, prefix := range legacyPrefixes { + if strings.HasPrefix(route, prefix) { + t.Errorf("legacy public route must not be advertised in OpenAPI: %s", route) + } + } + } + + required := []string{ + "/api/v1/healthz", + "/api/v1/readyz", + "/api/v1/openapi.json", + "/api/v1/chat/completions", + "/api/v1/responses", + "/api/v1/images/generations", + "/api/v1/videos/generations", + "/api/v1/models/{model}:generateContent", + "/api/v1/videos/omni-video", + "/api/v1/kling/v1/videos/omni-video", + "/api/v1/kling/v2/omni-video/{model}", + "/api/v1/contents/generations/tasks", + } + for _, route := range required { + if _, ok := document.Paths[route]; !ok { + t.Errorf("canonical public route missing from OpenAPI: %s", route) + } + } +} diff --git a/apps/api/internal/httpapi/server.go b/apps/api/internal/httpapi/server.go index 3802515..64bf06c 100644 --- a/apps/api/internal/httpapi/server.go +++ b/apps/api/internal/httpapi/server.go @@ -127,12 +127,16 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor mux := http.NewServeMux() mux.HandleFunc("GET /healthz", server.health) mux.HandleFunc("GET /readyz", server.ready) + mux.HandleFunc("GET /api/v1/healthz", server.health) + mux.HandleFunc("GET /api/v1/readyz", server.ready) mux.Handle("GET /metrics", securityEventMetrics.DynamicHandler(db)) mux.HandleFunc("GET /static/simulation/{asset}", serveSimulationAsset) mux.HandleFunc("GET /static/generated/{asset}", server.serveGeneratedStaticAsset) mux.HandleFunc("GET /static/uploaded/{asset}", server.serveUploadedStaticAsset) mux.HandleFunc("GET /api-docs-json", server.apiDocsJSON) mux.HandleFunc("GET /api-docs-yaml", server.apiDocsYAML) + mux.HandleFunc("GET /api/v1/openapi.json", server.apiDocsJSON) + mux.HandleFunc("GET /api/v1/openapi.yaml", server.apiDocsYAML) mux.HandleFunc("GET /api/v1/public/skills/ai-gateway-ops-management/metadata", server.getOpsManagementSkillMetadata) mux.HandleFunc("GET /api/v1/public/skills/ai-gateway-ops-management/download", server.downloadOpsManagementSkill) @@ -252,12 +256,12 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor mux.Handle("GET /api/admin/runtime/rate-limit-windows", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows))) mux.Handle("GET /api/admin/runtime/model-rate-limits", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listModelRateLimitStatuses))) mux.Handle("POST /api/v1/chat/completions", server.requireUser(auth.PermissionBasic, server.createAPIV1ChatCompletions())) - mux.Handle("POST /api/v1/responses", server.requireUser(auth.PermissionBasic, server.createTask("responses", false))) - mux.Handle("POST /api/v1/embeddings", server.requireUser(auth.PermissionBasic, server.createTask("embeddings", false))) - mux.Handle("POST /api/v1/reranks", server.requireUser(auth.PermissionBasic, server.createTask("reranks", false))) - mux.Handle("POST /api/v1/images/generations", server.requireUser(auth.PermissionBasic, server.createTask("images.generations", false))) - mux.Handle("POST /api/v1/images/edits", server.requireUser(auth.PermissionBasic, server.createTask("images.edits", false))) - mux.Handle("POST /api/v1/videos/generations", server.requireUser(auth.PermissionBasic, server.createTask("videos.generations", false))) + mux.Handle("POST /api/v1/responses", server.requireUser(auth.PermissionBasic, server.createTask("responses", true))) + mux.Handle("POST /api/v1/embeddings", server.requireUser(auth.PermissionBasic, server.createTask("embeddings", true))) + mux.Handle("POST /api/v1/reranks", server.requireUser(auth.PermissionBasic, server.createTask("reranks", true))) + mux.Handle("POST /api/v1/images/generations", server.requireUser(auth.PermissionBasic, server.createTask("images.generations", true))) + mux.Handle("POST /api/v1/images/edits", server.requireUser(auth.PermissionBasic, server.createTask("images.edits", true))) + mux.Handle("POST /api/v1/videos/generations", server.requireUser(auth.PermissionBasic, server.createTask("videos.generations", true))) mux.Handle("POST /api/v1/video/generations", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.createLegacyVolcesVideoGeneration))) mux.Handle("GET /api/v1/ai/result/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getLegacyVolcesVideoResult))) mux.Handle("POST /api/v1/song/generations", server.requireUser(auth.PermissionBasic, server.createTask("song.generations", true))) @@ -275,10 +279,16 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor mux.Handle("GET /api/v3/contents/generations/tasks", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listVolcesContentsGenerationTasks))) mux.Handle("GET /api/v3/contents/generations/tasks/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getVolcesContentsGenerationTask))) mux.Handle("DELETE /api/v3/contents/generations/tasks/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.deleteVolcesContentsGenerationTask))) + mux.Handle("POST /api/v1/contents/generations/tasks", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.createVolcesContentsGenerationTask))) + mux.Handle("GET /api/v1/contents/generations/tasks", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listVolcesContentsGenerationTasks))) + mux.Handle("GET /api/v1/contents/generations/tasks/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getVolcesContentsGenerationTask))) + mux.Handle("DELETE /api/v1/contents/generations/tasks/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.deleteVolcesContentsGenerationTask))) server.registerGeminiGenerateContentRoutes(mux) server.registerKlingCompatibilityRoutes(mux) mux.Handle("POST /upload/{version}/files", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.geminiFilesUpload))) mux.Handle("POST /upload/{version}/files/{uploadID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.geminiFilesUploadFinalize))) + mux.Handle("POST /api/v1/gemini/upload/{version}/files", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.geminiFilesUpload))) + mux.Handle("POST /api/v1/gemini/upload/{version}/files/{uploadID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.geminiFilesUploadFinalize))) mux.Handle("GET /api/v1/tasks", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listTasks))) mux.Handle("GET /api/v1/tasks/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getTask))) mux.Handle("POST /api/v1/tasks/{taskID}/cancel", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.cancelTask))) @@ -303,6 +313,8 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor mux.Handle("POST /v1/images/edits", server.requireUser(auth.PermissionBasic, server.createTask("images.edits", true))) mux.Handle("POST /v1/videos/omni-video", server.requireKelingAPIKey(http.HandlerFunc(server.createKelingOmniVideo))) mux.Handle("GET /v1/videos/omni-video/{taskID}", server.requireKelingAPIKey(http.HandlerFunc(server.getKelingOmniVideo))) + mux.Handle("POST /api/v1/videos/omni-video", server.requireKelingAPIKey(http.HandlerFunc(server.createKelingOmniVideo))) + mux.Handle("GET /api/v1/videos/omni-video/{taskID}", server.requireKelingAPIKey(http.HandlerFunc(server.getKelingOmniVideo))) mux.Handle("POST /song/generations", server.requireUser(auth.PermissionBasic, server.createTask("song.generations", true))) mux.Handle("POST /v1/song/generations", server.requireUser(auth.PermissionBasic, server.createTask("song.generations", true))) mux.Handle("POST /music/generations", server.requireUser(auth.PermissionBasic, server.createTask("music.generations", true))) diff --git a/apps/api/internal/httpapi/voice_clone_handlers.go b/apps/api/internal/httpapi/voice_clone_handlers.go index 11593bf..33a6ca1 100644 --- a/apps/api/internal/httpapi/voice_clone_handlers.go +++ b/apps/api/internal/httpapi/voice_clone_handlers.go @@ -16,8 +16,6 @@ import ( // @Failure 401 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/v1/voice_clone/voices [get] -// @Router /v1/voice_clone/voices [get] -// @Router /voice_clone/voices [get] func (s *Server) listClonedVoices(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok { @@ -51,8 +49,6 @@ func (s *Server) listClonedVoices(w http.ResponseWriter, r *http.Request) { // @Failure 404 {object} ErrorEnvelope // @Failure 502 {object} ErrorEnvelope // @Router /api/v1/voice_clone/voices/{voiceID} [delete] -// @Router /v1/voice_clone/voices/{voiceID} [delete] -// @Router /voice_clone/voices/{voiceID} [delete] func (s *Server) deleteClonedVoice(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok { diff --git a/apps/api/internal/httpapi/volces_compat_handlers.go b/apps/api/internal/httpapi/volces_compat_handlers.go index f089c77..3d28035 100644 --- a/apps/api/internal/httpapi/volces_compat_handlers.go +++ b/apps/api/internal/httpapi/volces_compat_handlers.go @@ -17,13 +17,13 @@ const volcesContentsCompatibilityMarker = "volces_contents_generations_v3" // createVolcesContentsGenerationTask godoc // @Summary 创建火山内容生成任务 -// @Description 兼容火山方舟 POST /api/v3/contents/generations/tasks。网关 task id 是查询与取消用的公开 id;上游 id 另以 upstream_task_id 保留。 +// @Description 统一公开入口兼容火山方舟内容生成任务。网关 task id 是查询与取消用的公开 id;上游 id 另以 upstream_task_id 保留。 // @Tags volces-compatible // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]any -// @Router /api/v3/contents/generations/tasks [post] +// @Router /api/v1/contents/generations/tasks [post] func (s *Server) createVolcesContentsGenerationTask(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok || user == nil { @@ -49,7 +49,7 @@ func (s *Server) createVolcesContentsGenerationTask(w http.ResponseWriter, r *ht // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]any -// @Router /api/v3/contents/generations/tasks/{taskID} [get] +// @Router /api/v1/contents/generations/tasks/{taskID} [get] func (s *Server) getVolcesContentsGenerationTask(w http.ResponseWriter, r *http.Request) { task, ok := s.volcesCompatibleTaskForUser(w, r) if !ok { @@ -64,7 +64,7 @@ func (s *Server) getVolcesContentsGenerationTask(w http.ResponseWriter, r *http. // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]any -// @Router /api/v3/contents/generations/tasks [get] +// @Router /api/v1/contents/generations/tasks [get] func (s *Server) listVolcesContentsGenerationTasks(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok || user == nil { @@ -105,7 +105,7 @@ func (s *Server) listVolcesContentsGenerationTasks(w http.ResponseWriter, r *htt // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]any -// @Router /api/v3/contents/generations/tasks/{taskID} [delete] +// @Router /api/v1/contents/generations/tasks/{taskID} [delete] func (s *Server) deleteVolcesContentsGenerationTask(w http.ResponseWriter, r *http.Request) { task, ok := s.volcesCompatibleTaskForUser(w, r) if !ok { diff --git a/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/SKILL.md b/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/SKILL.md index 169d4c0..19c5742 100644 --- a/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/SKILL.md +++ b/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/SKILL.md @@ -18,7 +18,7 @@ Use this skill to operate AI Gateway administration APIs through documented, evi - Reuse existing pricing rules, runtime policy sets, providers, protocol clients, base models, and platforms whenever their effective behavior satisfies the target. Do not create a near-duplicate resource merely because the upstream account, base URL, or provider-side model name differs. - Prefer a supported standard client before using `universal` scripts. Use custom scripts only when the upstream contract cannot be represented by the existing OpenAI, Gemini, or provider-specific clients. - Do not invent platform config fields or assume an arbitrary config key is enforced. For `universal`, use only the recognized keys documented in `references/model-universal-platforms.md`; treat any extra key as script-owned data available through `context.env`. -- Use the module references as the primary API source. Only when the required API is absent, inspect `/api-docs-json`; continue only when path, method, schema, authentication, permission, and side effects are unambiguous. +- Use the module references as the primary API source. Only when the required API is absent, inspect `/api/v1/openapi.json`; continue only when path, method, schema, authentication, permission, and side effects are unambiguous. ## Module Routing diff --git a/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/api-discovery-and-safety.md b/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/api-discovery-and-safety.md index c033f39..4980a26 100644 --- a/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/api-discovery-and-safety.md +++ b/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/api-discovery-and-safety.md @@ -2,7 +2,7 @@ ## Required Inputs -- Gateway API base URL. When using the bundled Web deployment this commonly includes `/gateway-api`; direct API access commonly uses port `8088`. +- Gateway origin and public API base URL. Public API access always ends with `/api/v1`; direct local access commonly uses `http://127.0.0.1:8088/api/v1`. - Administrator JWT with the `manager` or `admin` role. - Target provider documentation and authorization material. - Clear requested outcome and whether real upstream calls are allowed. @@ -10,7 +10,8 @@ Do not place credentials in files or reusable commands. Use shell environment variables: ```bash -export GATEWAY_BASE_URL='https://gateway.example.com/gateway-api' +export GATEWAY_ORIGIN='https://gateway.example.com' +export GATEWAY_PUBLIC_API_BASE="$GATEWAY_ORIGIN/api/v1" export GATEWAY_ADMIN_TOKEN='' ``` @@ -24,7 +25,7 @@ For standalone or hybrid deployments, local login can return a JWT: curl --fail-with-body \ -H 'Content-Type: application/json' \ -d '{"account":"","password":""}' \ - "$GATEWAY_BASE_URL/api/v1/auth/login" + "$GATEWAY_PUBLIC_API_BASE/auth/login" ``` Do not use local login when the deployment requires OIDC or server-main identity. Obtain the deployment's administrator access token instead. @@ -34,7 +35,7 @@ Verify identity and role before writes: ```bash curl --fail-with-body \ -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \ - "$GATEWAY_BASE_URL/api/v1/me" + "$GATEWAY_PUBLIC_API_BASE/me" ``` ## Request Pattern @@ -47,7 +48,7 @@ curl --fail-with-body \ -H 'Content-Type: application/json' \ -X POST \ -d '' \ - "$GATEWAY_BASE_URL/api/admin/" + "$GATEWAY_ORIGIN/api/admin/" ``` Always read current state before PATCH, DELETE, reset, disable, or full replacement. PATCH handlers for providers, base models, pricing rule sets, runtime policy sets, runner policy, and platforms write complete resource shapes rather than merging every omitted field. @@ -67,7 +68,7 @@ Obtain explicit confirmation after showing the current snapshot and impact befor The live machine-readable documents are: -- `/api-docs-json` -- `/api-docs-yaml` +- `/api/v1/openapi.json` +- `/api/v1/openapi.yaml` Use them only when this Skill does not document the required operation. Before acting, confirm the exact path, method, body, authentication, permission, response, and side effect. Do not infer a write operation from a similarly named endpoint. diff --git a/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/model-acceptance-runbook.md b/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/model-acceptance-runbook.md index 741db4d..7a3341c 100644 --- a/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/model-acceptance-runbook.md +++ b/apps/api/internal/skillbundle/content/skills/ai-gateway-ops-management/references/model-acceptance-runbook.md @@ -33,7 +33,7 @@ Use an authorized user JWT: ```bash curl --fail-with-body \ -H "Authorization: Bearer " \ - "$GATEWAY_BASE_URL/api/v1/model-catalog" + "$GATEWAY_PUBLIC_API_BASE/model-catalog" ``` Confirm model alias, model types, provider source, effective capabilities, pricing summary, rate limits, permissions, and enabled state. @@ -57,7 +57,7 @@ curl --fail-with-body \ "simulation": true, "stream": false }' \ - "$GATEWAY_BASE_URL/v1/chat/completions" + "$GATEWAY_PUBLIC_API_BASE/chat/completions" ``` Simulation verifies Gateway routing, permissions, parameter normalization, pricing, and task behavior, but it does not execute the real universal submit or poll scripts. Validate universal scripts separately against a local mock or approved provider test environment before enabling production traffic. @@ -82,4 +82,4 @@ With explicit approval, run one real minimal request and verify upstream request ## Final Report -Report resource IDs, before/after behavior, requests used for verification, simulation or real mode, billing evidence, remaining risks, rollback readiness, and whether `/api-docs-json` was used. Never include credentials or raw secret-bearing payloads. +Report resource IDs, before/after behavior, requests used for verification, simulation or real mode, billing evidence, remaining risks, rollback readiness, and whether `/api/v1/openapi.json` was used. Never include credentials or raw secret-bearing payloads. diff --git a/apps/api/project.json b/apps/api/project.json index 2eb2550..be25e56 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -24,7 +24,7 @@ "outputs": ["{projectRoot}/docs/swagger.json", "{projectRoot}/docs/swagger.yaml"], "options": { "cwd": "apps/api", - "command": "go run github.com/swaggo/swag/cmd/swag@v1.16.4 init --parseInternal -d ./cmd/gateway,./internal/httpapi,./internal/store,./internal/auth,./internal/identity -g main.go -o docs --outputTypes json,yaml" + "command": "go run github.com/swaggo/swag/cmd/swag@v1.16.4 init --parseInternal -d ./cmd/gateway,./internal/httpapi,./internal/store,./internal/auth,./internal/identity,./internal/runner -g main.go -o docs --outputTypes json,yaml" } }, "test": { diff --git a/apps/web/src/api.test.ts b/apps/web/src/api.test.ts index 8e4c220..4b7f88c 100644 --- a/apps/web/src/api.test.ts +++ b/apps/web/src/api.test.ts @@ -266,8 +266,8 @@ describe('Public Agent resources', () => { modules: ['model-runtime'], fileName: 'ai-gateway-ops-management-v1.0.2.zip', downloadPath: '/api/v1/public/skills/ai-gateway-ops-management/download', - apiDocsJsonPath: '/api-docs-json', - apiDocsYamlPath: '/api-docs-yaml', + apiDocsJsonPath: '/api/v1/openapi.json', + apiDocsYamlPath: '/api/v1/openapi.yaml', }; const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify(metadata), { status: 200, diff --git a/apps/web/src/pages/ApiDocsPage.tsx b/apps/web/src/pages/ApiDocsPage.tsx index 72f0aa0..f0901b5 100644 --- a/apps/web/src/pages/ApiDocsPage.tsx +++ b/apps/web/src/pages/ApiDocsPage.tsx @@ -35,17 +35,17 @@ interface ApiGuideItem { } export const apiDocs: ApiDocItem[] = [ - { key: 'chat', group: '文本', kind: 'chat.completions', method: 'POST', path: '/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' }, - { key: 'responses', group: '文本', kind: 'responses', method: 'POST', path: '/v1/responses', title: 'Responses', lead: 'OpenAI 兼容的 Responses 接口,原生支持 input、previous_response_id、工具调用和流式输出;不支持原生 Responses 的模型会由网关转换到 Chat Completions。' }, - { key: 'embeddings', group: '文本', kind: 'embeddings', method: 'POST', path: '/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口,可直接用 input 数组或字符串生成 embedding,API Key 需要 embedding 权限。' }, - { key: 'reranks', group: '文本', kind: 'reranks', method: 'POST', path: '/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口,传入 query 和 documents 后返回 relevance_score,API Key 需要 rerank 权限。' }, - { key: 'imageGeneration', group: '图片', kind: 'images.generations', method: 'POST', path: '/v1/images/generations', title: '创建图片', lead: 'OpenAI 兼容的图片生成接口,支持 prompt、size、quality 和 simulation 测试。' }, - { key: 'imageEdit', group: '图片', kind: 'images.edits', method: 'POST', path: '/v1/images/edits', title: '编辑图片', lead: 'OpenAI 兼容的图片编辑接口,支持 image、mask、prompt 和 simulation 测试。' }, + { key: 'chat', group: '文本', kind: 'chat.completions', method: 'POST', path: '/api/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' }, + { key: 'responses', group: '文本', kind: 'responses', method: 'POST', path: '/api/v1/responses', title: 'Responses', lead: 'OpenAI 兼容的 Responses 接口,原生支持 input、previous_response_id、工具调用和流式输出;不支持原生 Responses 的模型会由网关转换到 Chat Completions。' }, + { key: 'embeddings', group: '文本', kind: 'embeddings', method: 'POST', path: '/api/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口,可直接用 input 数组或字符串生成 embedding,API Key 需要 embedding 权限。' }, + { key: 'reranks', group: '文本', kind: 'reranks', method: 'POST', path: '/api/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口,传入 query 和 documents 后返回 relevance_score,API Key 需要 rerank 权限。' }, + { key: 'imageGeneration', group: '图片', kind: 'images.generations', method: 'POST', path: '/api/v1/images/generations', title: '创建图片', lead: 'OpenAI 兼容的图片生成接口,支持 prompt、size、quality 和 simulation 测试。' }, + { key: 'imageEdit', group: '图片', kind: 'images.edits', method: 'POST', path: '/api/v1/images/edits', title: '编辑图片', lead: 'OpenAI 兼容的图片编辑接口,支持 image、mask、prompt 和 simulation 测试。' }, { key: 'videoGeneration', group: '视频', kind: 'videos.generations', method: 'POST', path: '/api/v1/videos/generations', title: '生成视频', lead: '视频生成任务接口,支持文生视频、首尾帧、图片/视频/音频参考,以及时长、分辨率、画幅和声音等模型能力参数。' }, { key: 'asyncMode', group: '异步任务', title: '异步模式', lead: '所有 AI 任务创建接口使用同一种异步开启方式:保留原接口和原请求 Body,只需增加 X-Async: true。' }, { key: 'taskRetrieve', group: '异步任务', kind: 'tasks.retrieve', method: 'GET', path: '/api/v1/tasks/{taskID}', title: '取回任务', lead: '使用异步提交返回的 taskId 查询任务状态、结果、错误、用量、计费和执行尝试;queued、running、submitting 为进行中状态。' }, { key: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估', lead: '按请求体估算输入输出 token、模型倍率和折扣后的预估费用。' }, - { key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源,后续请求可复用返回的文件 URL。' }, + { key: 'files', group: '文件', method: 'POST', path: '/api/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源,后续请求可复用返回的文件 URL。' }, ]; const guideItems: ApiGuideItem[] = [ @@ -73,8 +73,8 @@ const defaultOpsSkillMetadata: GatewaySkillBundleMetadata = { modules: ['model-runtime'], fileName: 'ai-gateway-ops-management.zip', downloadPath: '/api/v1/public/skills/ai-gateway-ops-management/download', - apiDocsJsonPath: '/api-docs-json', - apiDocsYamlPath: '/api-docs-yaml', + apiDocsJsonPath: '/api/v1/openapi.json', + apiDocsYamlPath: '/api/v1/openapi.yaml', }; export function ApiDocsPage(props: { @@ -545,8 +545,8 @@ function GuideDetails(props: { onCreateApiKey: () => void; section: ApiGuideSect return ( <> -

Base URL 由当前 Gateway 部署环境提供。将接口文档中的路径拼接到 Base URL 后调用,不要重复添加末尾斜杠。

-
{`export EASYAI_BASE_URL="https://your-gateway.example.com"\ncurl "$EASYAI_BASE_URL/healthz"`}
+

公开 API Base URL 固定以 /api/v1 结尾。接口卡片展示的是完整路径;SDK 配置 Base URL 后只追加资源路径,不要再次添加 /api/v1

+
{`export EASYAI_BASE_URL="https://your-gateway.example.com/api/v1"\ncurl "$EASYAI_BASE_URL/healthz"`}

登录后在“用户工作台 → API Key”创建 Key,并为它分配实际需要的 chat、embedding、rerank、image 或 video 权限。密钥只在创建或重置时完整展示,请立即安全保存。

diff --git a/deploy/nginx/ai.51easyai.com-api-v1.inc b/deploy/nginx/ai.51easyai.com-api-v1.inc new file mode 100644 index 0000000..53d9866 --- /dev/null +++ b/deploy/nginx/ai.51easyai.com-api-v1.inc @@ -0,0 +1,19 @@ +# Include inside the TLS server block for ai.51easyai.com. +# The application owns routing below /api/v1; the edge proxy preserves the URI. + +location = /api/v1/metrics { + return 404; +} + +location ^~ /api/v1/ { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_pass http://127.0.0.1:8088; +} diff --git a/docker-compose.yml b/docker-compose.yml index 7422408..5b1f18b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,7 +87,7 @@ services: postgres: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8088/readyz | grep -q '\"ok\":true'"] + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8088/api/v1/readyz | grep -q '\"ok\":true'"] interval: 10s timeout: 5s retries: 20 @@ -117,7 +117,7 @@ services: api: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/gateway-api/healthz | grep -q 'easyai-ai-gateway'"] + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/api/v1/healthz | grep -q 'easyai-ai-gateway'"] interval: 10s timeout: 5s retries: 20 diff --git a/docker/nginx.conf b/docker/nginx.conf index a7ee56a..e1846e7 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -33,6 +33,10 @@ server { return 404; } + location = /api/v1/metrics { + return 404; + } + location = /gateway-api/api/v1/auth/login { proxy_http_version 1.1; proxy_set_header Host $host; @@ -47,6 +51,35 @@ server { proxy_pass http://api:8088/api/v1/auth/login; } + location = /api/v1/auth/login { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_connect_timeout 3s; + proxy_read_timeout 15s; + proxy_send_timeout 15s; + proxy_redirect off; + proxy_pass http://api:8088; + } + + # Canonical public API. Keep the request URI so /api/v1 reaches the + # versioned handlers without another prefix rewrite. + location ^~ /api/v1/ { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_pass http://api:8088; + } + location /gateway-api/ { proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/docs/design.md b/docs/design.md index 6f543e3..42f1b13 100644 --- a/docs/design.md +++ b/docs/design.md @@ -91,7 +91,7 @@ flowchart LR QUEUE --> PG QUEUE --> CALLBACK -->|POST task progress callback to server-main| WSGW QUEUE -->|settlement event| BILL - API -->|POST /v1/files/upload| FILES + API -->|POST /api/v1/files/upload| FILES ``` ## 4. Monorepo 方案 @@ -1785,9 +1785,9 @@ SimulationClient 根据 `simulation_profile` 生成确定性行为: - `/chat/completions` - `/images/generations` - `/video/generations` -- `/v1/chat/completions` -- `/v1/images/generations` -- `/v1/video/generations` +- `/api/v1/chat/completions` +- `/api/v1/images/generations` +- `/api/v1/video/generations` 内部 `OpenaiService` 变成薄门面: diff --git a/docs/kling-compatible-api.md b/docs/kling-compatible-api.md index e42c557..d1286cf 100644 --- a/docs/kling-compatible-api.md +++ b/docs/kling-compatible-api.md @@ -5,11 +5,11 @@ 生产环境统一配置: ```text -baseURL = https://ai.51easyai.com/gateway-api/kling +baseURL = https://ai.51easyai.com/api/v1/kling Authorization = Bearer ``` -本地环境使用 `baseURL = http://localhost:8088/kling`。 +本地环境使用 `baseURL = http://localhost:8088/api/v1/kling`。旧 `/gateway-api/kling` 和 `/kling` 路径仅作为兼容别名保留。 ## V1(AK/SK 旧版协议兼容) diff --git a/docs/kling-omni-compatible.md b/docs/kling-omni-compatible.md index 29acf9f..dd01753 100644 --- a/docs/kling-omni-compatible.md +++ b/docs/kling-omni-compatible.md @@ -2,6 +2,12 @@ EasyAI AI Gateway 提供 Kling 旧版 Omni 协议兼容接口。调用方继续使用 Gateway API Key,任务仍经过网关候选选择、异步队列、审计和计费;响应中的 `task_id` 是网关任务 UUID,不是上游任务 ID。 +```bash +export GATEWAY_ORIGIN="https://ai.51easyai.com" +export GATEWAY_PUBLIC_API_BASE="$GATEWAY_ORIGIN/api/v1" +export GATEWAY_API_KEY="" +``` + ## 模型与参数映射 | 请求 `model_name` | 网关模型别名 | TranStreams 原生 `model_name` | 时长范围 | @@ -21,10 +27,10 @@ EasyAI AI Gateway 提供 Kling 旧版 Omni 协议兼容接口。调用方继续 ## 创建任务 -`POST /v1/videos/omni-video` 固定异步受理,不需要 `X-Async`,成功返回 HTTP 200。 +`POST /api/v1/videos/omni-video` 固定异步受理,不需要 `X-Async`,成功返回 HTTP 200。 ```bash -curl -sS -X POST "$GATEWAY_BASE_URL/v1/videos/omni-video" \ +curl -sS -X POST "$GATEWAY_ORIGIN/api/v1/videos/omni-video" \ -H "Authorization: Bearer $GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ @@ -61,7 +67,7 @@ curl -sS -X POST "$GATEWAY_BASE_URL/v1/videos/omni-video" \ ```bash curl -sS \ -H "Authorization: Bearer $GATEWAY_API_KEY" \ - "$GATEWAY_BASE_URL/v1/videos/omni-video/$TASK_ID" + "$GATEWAY_ORIGIN/api/v1/videos/omni-video/$TASK_ID" ``` `task_status` 为 `submitted`、`processing`、`succeed` 或 `failed`。成功时结果位于 `data.task_result.videos`: @@ -93,7 +99,7 @@ curl -sS \ 标准接口仍为 `POST /api/v1/videos/generations`。异步调用需要 `X-Async: true`,再通过 `GET /api/v1/tasks/{taskId}` 轮询。 ```bash -curl -sS -X POST "$GATEWAY_BASE_URL/api/v1/videos/generations" \ +curl -sS -X POST "$GATEWAY_PUBLIC_API_BASE/videos/generations" \ -H "Authorization: Bearer $GATEWAY_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Async: true" \ @@ -112,7 +118,7 @@ curl -sS -X POST "$GATEWAY_BASE_URL/api/v1/videos/generations" \ ```bash curl -sS \ -H "Authorization: Bearer $GATEWAY_API_KEY" \ - "$GATEWAY_BASE_URL/api/v1/tasks/$TASK_ID" + "$GATEWAY_PUBLIC_API_BASE/tasks/$TASK_ID" ``` ## 错误格式 @@ -129,4 +135,4 @@ curl -sS \ 业务码分类:`1001/1002` 为鉴权错误,`1101/1103` 为余额或权限错误,`1201/1203` 为参数或资源错误,`1302/1303` 为限流错误,`5000/5001` 为网关或上游服务错误。HTTP 状态码仍反映错误类型。 -OpenAPI 文档由服务的 `/openapi.json` 和 `/openapi.yaml` 提供。 +OpenAPI 文档由服务的 `/api/v1/openapi.json` 和 `/api/v1/openapi.yaml` 提供。 diff --git a/docs/public-api-v1.md b/docs/public-api-v1.md new file mode 100644 index 0000000..e449343 --- /dev/null +++ b/docs/public-api-v1.md @@ -0,0 +1,137 @@ +# EasyAI Gateway 公开 API V1 清单 + +生产公开 API 的统一 Base URL: + +```text +https://ai.51easyai.com/api/v1 +``` + +下表路径均以 `/api/v1` 开头。调用方使用 `Authorization: Bearer `;标记为“公开”的接口不要求用户登录,OIDC 与 SSF 接口按各自协议鉴权。 + +旧 `/gateway-api`、`/v1`、无版本路径、`/kling`、`/v1beta`、`/upload` 和 `/api/v3` 入口仅作为兼容别名保留,不再用于新接入文档。 + +## 运行状态与接口发现 + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/v1/healthz` | 存活检查 | +| GET | `/api/v1/readyz` | 就绪检查 | +| GET | `/api/v1/openapi.json` | OpenAPI JSON | +| GET | `/api/v1/openapi.yaml` | OpenAPI YAML | +| GET | `/api/v1/public/identity` | 公开身份配置 | +| GET | `/api/v1/public/client-customization` | 公开客户端配置 | +| GET | `/api/v1/public/catalog/providers` | 公开供应商目录 | +| GET | `/api/v1/public/catalog/base-models` | 公开基础模型目录 | +| GET | `/api/v1/public/skills/ai-gateway-ops-management/metadata` | 运维 Skill 元数据 | +| GET | `/api/v1/public/skills/ai-gateway-ops-management/download` | 下载运维 Skill | + +## 账号、授权与 API Key + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/v1/auth/register` | 注册本地账号 | +| POST | `/api/v1/auth/login` | 本地账号登录 | +| GET | `/api/v1/auth/oidc/login` | 发起 OIDC 登录 | +| GET | `/api/v1/auth/oidc/callback` | OIDC 回调 | +| POST | `/api/v1/auth/oidc/logout` | OIDC 登出 | +| DELETE | `/api/v1/auth/oidc/session` | 删除浏览器会话 | +| GET | `/api/v1/me` | 当前用户 | +| GET, POST | `/api/v1/api-keys` | 查询、创建 API Key | +| GET | `/api/v1/api-keys/access-rules` | 查询 Key 访问规则 | +| POST | `/api/v1/api-keys/access-rules/batch` | 批量设置 Key 访问规则 | +| GET | `/api/v1/api-keys/assignable-models` | 查询可分配模型 | +| PATCH | `/api/v1/api-keys/{apiKeyID}/scopes` | 更新 Key 权限范围 | +| PATCH | `/api/v1/api-keys/{apiKeyID}/disable` | 禁用 Key | +| DELETE | `/api/v1/api-keys/{apiKeyID}` | 删除 Key | + +## 模型、平台与计费查询 + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/v1/model-catalog` | 模型能力目录 | +| GET | `/api/v1/platforms` | 当前用户可用平台 | +| GET | `/api/v1/models` | 当前用户可用模型 | +| GET | `/api/v1/playground/models` | Playground 可用模型 | +| POST | `/api/v1/pricing/estimate` | 请求价格预估 | + +## 通用与 OpenAI 兼容生成接口 + +这些接口默认同步返回兼容响应;需要异步执行时增加 `X-Async: true`,并使用任务接口取回结果。 + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/v1/chat/completions` | Chat Completions,支持 SSE | +| POST | `/api/v1/responses` | Responses,支持 SSE | +| POST | `/api/v1/embeddings` | 文本向量 | +| POST | `/api/v1/reranks` | 文本重排序 | +| POST | `/api/v1/images/generations` | 文生图 | +| POST | `/api/v1/images/edits` | 图片编辑 | +| POST | `/api/v1/videos/generations` | 文生视频、图生视频及多模态视频 | +| POST | `/api/v1/song/generations` | 歌曲生成 | +| POST | `/api/v1/music/generations` | 音乐生成 | +| POST | `/api/v1/speech/generations` | 语音生成 | +| POST | `/api/v1/voice_clone` | 声音克隆 | +| GET | `/api/v1/voice_clone/voices` | 查询克隆声音 | +| DELETE | `/api/v1/voice_clone/voices/{voiceID}` | 删除克隆声音 | +| POST | `/api/v1/files/upload` | 上传生成任务输入文件 | + +## 异步任务 + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/v1/tasks` | 查询任务列表 | +| GET | `/api/v1/tasks/{taskID}` | 查询任务详情和结果 | +| POST | `/api/v1/tasks/{taskID}/cancel` | 取消任务 | +| GET | `/api/v1/tasks/{taskID}/events` | 查询任务事件 | +| GET | `/api/v1/tasks/{taskID}/param-preprocessing` | 查询参数预处理记录 | + +## Gemini 兼容接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/v1/models/{model}:generateContent` | Gemini generateContent | +| POST | `/api/v1/gemini/upload/{version}/files` | Gemini Files 启动或直接上传,`version` 为 `v1` 或 `v1beta` | +| POST | `/api/v1/gemini/upload/{version}/files/{uploadID}` | 完成 Gemini 分段上传 | + +## 可灵兼容接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/v1/videos/omni-video` | 可灵官方 V1 Omni 创建任务 | +| GET | `/api/v1/videos/omni-video/{taskID}` | 可灵官方 V1 Omni 查询任务 | +| POST | `/api/v1/kling/v1/videos/omni-video` | 网关可灵 V1 创建任务 | +| GET | `/api/v1/kling/v1/videos/omni-video` | 网关可灵 V1 任务列表 | +| GET | `/api/v1/kling/v1/videos/omni-video/{taskID}` | 网关可灵 V1 查询任务 | +| POST | `/api/v1/kling/v2/omni-video/{model}` | 网关可灵 API 2.0 创建任务 | +| GET | `/api/v1/kling/v2/tasks` | 网关可灵 API 2.0 查询任务 | +| POST | `/api/v1/kling/v2/tasks` | 网关可灵 API 2.0 任务列表 | + +可灵客户端可使用 `https://ai.51easyai.com/api/v1/kling` 作为 Base URL,然后继续请求 `/v1/...` 或 `/v2/...`。 + +## 火山兼容与真人资产 + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/v1/contents/generations/tasks` | 创建火山内容生成任务 | +| GET | `/api/v1/contents/generations/tasks` | 查询火山内容生成任务列表 | +| GET | `/api/v1/contents/generations/tasks/{taskID}` | 查询火山内容生成任务 | +| DELETE | `/api/v1/contents/generations/tasks/{taskID}` | 删除或取消火山内容生成任务 | +| POST | `/api/v1/video/generations` | server-main 兼容视频创建接口 | +| GET | `/api/v1/ai/result/{taskID}` | server-main 兼容结果查询接口 | +| GET | `/api/v1/resource/material/seedance-portrait-assets/capability` | 真人资产能力 | +| GET | `/api/v1/resource/material/user/materials` | 查询用户真人资产 | +| POST | `/api/v1/resource/material` | 上传真人资产 | +| POST | `/api/v1/resource/material/seedance-portrait-assets/sync` | 同步真人资产到平台 | + +## 安全集成 + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/v1/security-events/ssf` | RFC 8935 Security Event 接收端点 | + +## 不属于公开 API 的路径 + +- `/api/admin/...`:管理后台接口。 +- `/api/workspace/...`、`/api/playground/...`:Web/BFF 内部接口。 +- `/metrics`:仅监控网络可访问。 +- `/static/...`:生成结果和上传文件的资源 URL,不是 API Base URL。 diff --git a/docs/runbooks/production-ci-cd.md b/docs/runbooks/production-ci-cd.md index 1fbd25c..9cc9108 100644 --- a/docs/runbooks/production-ci-cd.md +++ b/docs/runbooks/production-ci-cd.md @@ -126,9 +126,11 @@ dispatcher 以完整 Git SHA 发布 Registry Tag,并把 Registry 返回的 dig ## 发布后验证 +宿主 Nginx 的 `ai.51easyai.com` TLS server 必须包含仓库中的 `deploy/nginx/ai.51easyai.com-api-v1.inc` 等价规则,保留完整 URI 转发到 `127.0.0.1:8088`。修改前先备份现有配置,执行 `nginx -t` 成功后才能 reload。 + ```bash -curl -fsS https://ai.51easyai.com/gateway-api/healthz -curl -fsS https://ai.51easyai.com/gateway-api/readyz +curl -fsS https://ai.51easyai.com/api/v1/healthz +curl -fsS https://ai.51easyai.com/api/v1/readyz ssh root@110.42.51.33 'cd /root/easyai-ai-gateway-deploy && ./gateway-ops.sh ps' ``` diff --git a/docs/seedance-portrait-assets.md b/docs/seedance-portrait-assets.md index f046592..4071a2d 100644 --- a/docs/seedance-portrait-assets.md +++ b/docs/seedance-portrait-assets.md @@ -33,10 +33,12 @@ ## 火山任务兼容路由 -- `POST /api/v3/contents/generations/tasks` -- `GET /api/v3/contents/generations/tasks` -- `GET /api/v3/contents/generations/tasks/{taskID}` -- `DELETE /api/v3/contents/generations/tasks/{taskID}` +- `POST /api/v1/contents/generations/tasks` +- `GET /api/v1/contents/generations/tasks` +- `GET /api/v1/contents/generations/tasks/{taskID}` +- `DELETE /api/v1/contents/generations/tasks/{taskID}` + +旧 `/api/v3/contents/generations/tasks` 路径仅作为火山客户端兼容别名保留。 列表接口兼容火山的 `page_num`、`page_size`、`filter.status`、`filter.task_ids`(可重复)和 `filter.model`,并返回官方 `items`、`total` 字段;`data`、`page` 是保留的网关附加字段。 diff --git a/docs/server-main-integration.md b/docs/server-main-integration.md index b7b840f..85f3971 100644 --- a/docs/server-main-integration.md +++ b/docs/server-main-integration.md @@ -36,7 +36,7 @@ Content-Type: application/json ### 1.2 文件上传 ```http -POST /v1/files/upload +POST /api/v1/files/upload Authorization: Bearer ${USER_JWT_OR_SK} Content-Type: multipart/form-data diff --git a/docs/test/loopback-test-checklist.md b/docs/test/loopback-test-checklist.md index dbe124f..2061a0a 100644 --- a/docs/test/loopback-test-checklist.md +++ b/docs/test/loopback-test-checklist.md @@ -53,7 +53,7 @@ | ID | 任务 | 接口 / 方式 | 成功判定 | 状态 | 结果记录 | | --- | --- | --- | --- | --- | --- | -| SETUP-01 | 确认服务可用 | `GET /healthz`、`GET /readyz` | `healthz.ok=true`,`readyz.ok=true` | 未执行 | 待填写 | +| SETUP-01 | 确认服务可用 | `GET /api/v1/healthz`、`GET /api/v1/readyz` | `healthz.ok=true`,`readyz.ok=true` | 未执行 | 待填写 | | SETUP-02 | 准备管理员权限 | 本地注册 / 登录,必要时将测试用户提升为 `admin` 或 `manager` | `GET /api/v1/me` 返回 `role` 具备 `manager` 权限 | 未执行 | 待填写 | | SETUP-03 | 记录用户提供的真实平台、模型和 KEY | `GET /api/v1/platforms`、`GET /api/v1/models` | Chat 模型、`doubao-4.5图像编辑`、`豆包Seedance-1.5-pro` 均已启用,并能被管理员看到 | 未执行 | 待填写 | | SETUP-04 | 创建内部测试用户组 | `POST /api/v1/user-groups` | 创建 `loopback-allow-group`、`loopback-deny-group`、`loopback-limit-group` | 未执行 | 待填写 | @@ -91,7 +91,7 @@ | ID | 能力 | 请求 | 成功判定 | 状态 | 结果记录 | | --- | --- | --- | --- | --- | --- | | TASK-CHAT-01 | Chat 成功 | `POST /api/v1/chat/completions`,真实 Chat 模型 | `task.status=succeeded`,`result.object=chat.completion`,`choices[0].message.content` 非空 | 未执行 | taskId、requestId、content 摘要、charge 待填写 | -| TASK-CHAT-02 | Chat 兼容路由成功 | `POST /v1/chat/completions`,真实 Chat 模型 | HTTP 200,返回 `object=chat.completion`,内容非空 | 未执行 | requestId、content 摘要待填写 | +| TASK-CHAT-02 | Chat 同步兼容响应成功 | `POST /api/v1/chat/completions`,真实 Chat 模型 | HTTP 200,返回 `object=chat.completion`,内容非空 | 未执行 | requestId、content 摘要待填写 | | TASK-IMAGE-01 | 文生图成功 | `POST /api/v1/images/generations`,模型 `doubao-4.5图像编辑` 或用户补充的文生图模型 | `task.status=succeeded`,`result.id` 非空,`data[0].url` 或文件 URL 可访问 | 未执行 | taskId、image URL、charge 待填写 | | TASK-IMAGE-02 | 图生图成功 | `POST /api/v1/images/edits`,模型 `doubao-4.5图像编辑`,传入测试源图和 mask | `task.status=succeeded`,`result.id` 非空,`data[0].url` 或文件 URL 可访问 | 未执行 | taskId、source URL、mask URL、image URL、charge 待填写 | | TASK-VIDEO-01 | 文生视频成功 | `POST /api/v1/videos/generations`,模型 `豆包Seedance-1.5-pro`,仅传 prompt | `task.status=succeeded`,返回可下载或可播放的视频结果,任务事件完整 | 未执行 | taskId、video URL、duration、charge 待填写 | From 5432760cf7142f7b4048058a202ff280f8489b87 Mon Sep 17 00:00:00 2001 From: easyai Date: Wed, 22 Jul 2026 08:48:36 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(identity):=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=9C=AA=E9=85=8D=E7=BD=AE=20SSF=20=E6=97=B6=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E7=A9=BA=E6=8C=87=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SecurityEventReceiver 不再把空 ConnectionManager 包装为非空接口;未启用 SSF 时公开端点明确返回 404。补充运行时和 HTTP 回归测试。\n\n验证:go test ./internal/identityruntime ./internal/httpapi。 --- apps/api/internal/httpapi/security_events_test.go | 12 ++++++++++++ apps/api/internal/identityruntime/manager.go | 5 ++++- apps/api/internal/identityruntime/manager_test.go | 8 ++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/api/internal/httpapi/security_events_test.go b/apps/api/internal/httpapi/security_events_test.go index ab0e162..59b913a 100644 --- a/apps/api/internal/httpapi/security_events_test.go +++ b/apps/api/internal/httpapi/security_events_test.go @@ -40,6 +40,18 @@ func TestReceiveSecurityEventUsesPreparedReceiverBeforeFirstActivation(t *testin } } +func TestReceiveSecurityEventReturnsNotFoundWithoutConfiguredReceiver(t *testing.T) { + server := &Server{identityRuntime: identityruntime.NewManager(nil, &preparedReceiverBuilder{})} + request := httptest.NewRequest(http.MethodPost, "/api/v1/security-events/ssf", nil) + response := httptest.NewRecorder() + + server.receiveSecurityEvent(response, request) + + if response.Code != http.StatusNotFound { + t.Fatalf("unconfigured SSF status=%d, want %d", response.Code, http.StatusNotFound) + } +} + func TestSecurityEventWriteAuditFailsClosedBeforeMutation(t *testing.T) { server := &Server{} request := httptest.NewRequest(http.MethodPost, "/api/admin/system/security-events/connection/verify", nil) diff --git a/apps/api/internal/identityruntime/manager.go b/apps/api/internal/identityruntime/manager.go index 9060bad..2460b5d 100644 --- a/apps/api/internal/identityruntime/manager.go +++ b/apps/api/internal/identityruntime/manager.go @@ -105,7 +105,10 @@ func (manager *Manager) SecurityEventReceiver() http.Handler { if runtime := manager.Current(); runtime != nil && runtime.SecurityEvents != nil { return runtime.SecurityEvents } - return manager.SecurityEventManager() + if securityEventManager := manager.SecurityEventManager(); securityEventManager != nil { + return securityEventManager + } + return nil } // SecurityEventManager resolves the manager used by administrative recovery diff --git a/apps/api/internal/identityruntime/manager_test.go b/apps/api/internal/identityruntime/manager_test.go index 6ef7dac..fbd00d6 100644 --- a/apps/api/internal/identityruntime/manager_test.go +++ b/apps/api/internal/identityruntime/manager_test.go @@ -630,6 +630,14 @@ func TestSecurityEventManagerExposesPreparedRecoveryManagerWithoutActiveRuntime( } } +func TestSecurityEventReceiverReturnsNilWithoutConfiguredManager(t *testing.T) { + manager := NewManager(&runtimeRepositoryFake{}, &runtimeBuilderFake{}) + + if manager.SecurityEventReceiver() != nil { + t.Fatal("unconfigured security event receiver should be nil") + } +} + func TestSecurityEventManagerPrefersActiveRuntime(t *testing.T) { active := &securityevents.ConnectionManager{} prepared := &securityevents.ConnectionManager{} From a8d1c550effcbf9136422ec85b5fcaaf2655b285 Mon Sep 17 00:00:00 2001 From: easyai Date: Wed, 22 Jul 2026 08:48:40 +0800 Subject: [PATCH 3/5] =?UTF-8?q?chore(deps):=20=E5=8D=87=E7=BA=A7=20fast-ur?= =?UTF-8?q?i=20=E4=BF=AE=E5=A4=8D=E9=AB=98=E5=8D=B1=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将构建链路中的 fast-uri 锁定到 3.1.4,修复 host confusion 高危公告,不改变应用运行时依赖。\n\n验证:pnpm install --frozen-lockfile;pnpm audit --audit-level high;pnpm lint;pnpm test;pnpm build。 --- pnpm-lock.yaml | 10 +++++----- pnpm-workspace.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1314d12..ed3accd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ overrides: brace-expansion@>=2.0.0 <2.1.2: 2.1.2 dompurify@<=3.4.10: 3.4.11 esbuild@>=0.27.3 <0.28.1: 0.28.1 - fast-uri@>=3.0.0 <3.1.3: 3.1.3 + fast-uri@>=3.0.0 <3.1.4: 3.1.4 form-data@>=4.0.0 <4.0.6: 4.0.6 js-yaml@<4.3.0: 4.3.0 mermaid@>=11.0.0-alpha.1 <=11.14.0: 11.15.0 @@ -3035,8 +3035,8 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-uri@3.1.3: - resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -7182,7 +7182,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -7851,7 +7851,7 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-uri@3.1.3: {} + fast-uri@3.1.4: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f407d9b..3238170 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,7 +12,7 @@ overrides: 'brace-expansion@>=2.0.0 <2.1.2': 2.1.2 'dompurify@<=3.4.10': 3.4.11 'esbuild@>=0.27.3 <0.28.1': 0.28.1 - 'fast-uri@>=3.0.0 <3.1.3': 3.1.3 + 'fast-uri@>=3.0.0 <3.1.4': 3.1.4 'form-data@>=4.0.0 <4.0.6': 4.0.6 'js-yaml@<4.3.0': 4.3.0 'mermaid@>=11.0.0-alpha.1 <=11.14.0': 11.15.0 From eb37b568ae7e62fd3d623ebdeeda7448fcd0452a Mon Sep 17 00:00:00 2001 From: easyai Date: Wed, 22 Jul 2026 08:50:55 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(deploy):=20=E4=BD=BF=E7=94=A8=20/api/v1?= =?UTF-8?q?=20=E6=89=A7=E8=A1=8C=E5=8F=91=E5=B8=83=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Compose 发布脚本的 API、就绪和 Web 反代探针切换到统一公开前缀,避免发布完成后仍只验证旧兼容入口。\n\n验证:bash -n;ShellCheck;ci-build-images-test.sh;pipeline-test.sh。 --- scripts/deploy-compose.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/deploy-compose.sh b/scripts/deploy-compose.sh index 919df75..618747d 100755 --- a/scripts/deploy-compose.sh +++ b/scripts/deploy-compose.sh @@ -248,14 +248,14 @@ deploy() { api_port="$(published_port api 8088 "${AI_GATEWAY_API_PORT:-8088}")" web_port="$(published_port web 80 "${AI_GATEWAY_WEB_PORT:-5178}")" - wait_for_http "api health" "http://127.0.0.1:${api_port}/healthz" "easyai-ai-gateway" - wait_for_http "api readiness" "http://127.0.0.1:${api_port}/readyz" '"ok":true' - wait_for_http "web reverse proxy" "http://127.0.0.1:${web_port}/gateway-api/healthz" "easyai-ai-gateway" + wait_for_http "api health" "http://127.0.0.1:${api_port}/api/v1/healthz" "easyai-ai-gateway" + wait_for_http "api readiness" "http://127.0.0.1:${api_port}/api/v1/readyz" '"ok":true' + wait_for_http "web reverse proxy" "http://127.0.0.1:${web_port}/api/v1/healthz" "easyai-ai-gateway" wait_for_http "web app" "http://127.0.0.1:${web_port}/" "EasyAI AI Gateway" echo "[ai-gateway] deployment succeeded" echo "[ai-gateway] Web: http://127.0.0.1:${web_port}" - echo "[ai-gateway] API: http://127.0.0.1:${api_port}/healthz" + echo "[ai-gateway] API: http://127.0.0.1:${api_port}/api/v1/healthz" } case "$ACTION" in From a23b28c27d2735094c015946715bb936b971bda1 Mon Sep 17 00:00:00 2001 From: easyai Date: Wed, 22 Jul 2026 08:56:30 +0800 Subject: [PATCH 5/5] =?UTF-8?q?test(api):=20=E7=AD=89=E5=BE=85=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E4=BB=BB=E5=8A=A1=E5=AE=8C=E6=88=90=E5=90=8E=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真实队列环境中 X-Async 请求会先返回 queued。集成测试改为轮询任务完成并重新读取详情,避免把正确的异步受理状态误判为失败。 --- apps/api/internal/httpapi/core_flow_integration_test.go | 9 +++++++++ .../httpapi/keling_simulation_integration_test.go | 5 +++++ .../internal/httpapi/kling_compat_integration_test.go | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/apps/api/internal/httpapi/core_flow_integration_test.go b/apps/api/internal/httpapi/core_flow_integration_test.go index 59dc203..d9f4b0c 100644 --- a/apps/api/internal/httpapi/core_flow_integration_test.go +++ b/apps/api/internal/httpapi/core_flow_integration_test.go @@ -513,6 +513,8 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil { "simulation": true, "simulationDurationMs": 5, }, map[string]string{"X-Async": "true"}, http.StatusAccepted, &imageResponse) + waitForTaskStatus(t, server.URL, apiKeyResponse.Secret, imageResponse.Task.ID, []string{"succeeded"}, 10*time.Second) + doJSON(t, server.URL, http.MethodGet, "/api/v1/tasks/"+imageResponse.Task.ID, apiKeyResponse.Secret, nil, http.StatusOK, &imageResponse.Task) if imageResponse.Task.Status != "succeeded" || imageResponse.Task.Result["id"] == "" { t.Fatalf("unexpected image generation task: %+v", imageResponse.Task) } @@ -533,6 +535,8 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil { "simulation": true, "simulationDurationMs": 5, }, map[string]string{"X-Async": "true"}, http.StatusAccepted, &imageEditResponse) + waitForTaskStatus(t, server.URL, apiKeyResponse.Secret, imageEditResponse.Task.ID, []string{"succeeded"}, 10*time.Second) + doJSON(t, server.URL, http.MethodGet, "/api/v1/tasks/"+imageEditResponse.Task.ID, apiKeyResponse.Secret, nil, http.StatusOK, &imageEditResponse.Task) if imageEditResponse.Task.Status != "succeeded" || imageEditResponse.Task.Result["id"] == "" { t.Fatalf("unexpected image edit task: %+v", imageEditResponse.Task) } @@ -1196,6 +1200,7 @@ WHERE reference_type = 'gateway_task' }, http.StatusCreated, &videoRoutePlatformModel) var textToVideoTask struct { Task struct { + ID string `json:"id"` Status string `json:"status"` ModelType string `json:"modelType"` } `json:"task"` @@ -1207,6 +1212,8 @@ WHERE reference_type = 'gateway_task' "simulationDurationMs": 5, "prompt": "text to video route", }, map[string]string{"X-Async": "true"}, http.StatusAccepted, &textToVideoTask) + waitForTaskStatus(t, server.URL, apiKeyResponse.Secret, textToVideoTask.Task.ID, []string{"succeeded"}, 10*time.Second) + doJSON(t, server.URL, http.MethodGet, "/api/v1/tasks/"+textToVideoTask.Task.ID, apiKeyResponse.Secret, nil, http.StatusOK, &textToVideoTask.Task) if textToVideoTask.Task.Status != "succeeded" || textToVideoTask.Task.ModelType != "video_generate" { t.Fatalf("text-to-video request should use video_generate model_type: %+v", textToVideoTask.Task) } @@ -1226,6 +1233,8 @@ WHERE reference_type = 'gateway_task' "prompt": "image to video route", "image": "https://example.com/source.png", }, map[string]string{"X-Async": "true"}, http.StatusAccepted, &imageToVideoTask) + waitForTaskStatus(t, server.URL, apiKeyResponse.Secret, imageToVideoTask.Task.ID, []string{"succeeded"}, 10*time.Second) + doJSON(t, server.URL, http.MethodGet, "/api/v1/tasks/"+imageToVideoTask.Task.ID, apiKeyResponse.Secret, nil, http.StatusOK, &imageToVideoTask.Task) if imageToVideoTask.Task.Status != "succeeded" || imageToVideoTask.Task.ModelType != "image_to_video" { t.Fatalf("image-to-video request should use image_to_video model_type: %+v", imageToVideoTask.Task) } diff --git a/apps/api/internal/httpapi/keling_simulation_integration_test.go b/apps/api/internal/httpapi/keling_simulation_integration_test.go index ecdca2d..66eb657 100644 --- a/apps/api/internal/httpapi/keling_simulation_integration_test.go +++ b/apps/api/internal/httpapi/keling_simulation_integration_test.go @@ -145,6 +145,11 @@ WHERE username = $1`, username); err != nil { http.StatusAccepted, &response, ) + if response.Task.ID == "" { + t.Fatal("async Kling simulation response did not return a task id") + } + waitForTaskStatus(t, server.URL, apiKeyResponse.Secret, response.Task.ID, []string{"succeeded"}, 10*time.Second) + doJSON(t, server.URL, http.MethodGet, "/api/v1/tasks/"+response.Task.ID, apiKeyResponse.Secret, nil, http.StatusOK, &response.Task) task := response.Task if task.ID == "" || diff --git a/apps/api/internal/httpapi/kling_compat_integration_test.go b/apps/api/internal/httpapi/kling_compat_integration_test.go index 15cc410..9b9afb5 100644 --- a/apps/api/internal/httpapi/kling_compat_integration_test.go +++ b/apps/api/internal/httpapi/kling_compat_integration_test.go @@ -138,12 +138,18 @@ WHERE platform_id = $1::uuid } var response struct { Task struct { + ID string `json:"id"` Status string `json:"status"` ModelType string `json:"modelType"` ResolvedModel string `json:"resolvedModel"` } `json:"task"` } doJSONWithHeaders(t, server.URL, http.MethodPost, "/api/v1/videos/generations", apiKeyResponse.Secret, request, map[string]string{"X-Async": "true"}, http.StatusAccepted, &response) + if response.Task.ID == "" { + t.Fatal("async generic video response did not return a task id") + } + waitForTaskStatus(t, server.URL, apiKeyResponse.Secret, response.Task.ID, []string{"succeeded"}, 10*time.Second) + doJSON(t, server.URL, http.MethodGet, "/api/v1/tasks/"+response.Task.ID, apiKeyResponse.Secret, nil, http.StatusOK, &response.Task) resolvedModel, resolved := klingV2ProviderModel(response.Task.ResolvedModel) if response.Task.Status != "succeeded" || response.Task.ModelType != expectedModelType || !resolved || resolvedModel != model { t.Fatalf("generic video request without modelType should use inferred capability: %+v", response.Task)