完善文档页文本向量与重排序调用支持
This commit is contained in:
@@ -4081,6 +4081,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/files/upload": {
|
"/api/v1/files/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -4641,6 +4734,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/responses": {
|
"/api/v1/responses": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -5533,6 +5719,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {
|
"/healthz": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。",
|
"description": "返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。",
|
||||||
@@ -5765,6 +6044,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {
|
"/responses": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -6057,6 +6429,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {
|
"/v1/files/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -6311,6 +6776,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {
|
"/v1/responses": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|||||||
@@ -4912,6 +4912,67 @@ paths:
|
|||||||
summary: 创建 Chat Completions
|
summary: 创建 Chat Completions
|
||||||
tags:
|
tags:
|
||||||
- tasks
|
- tasks
|
||||||
|
/api/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: 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
|
||||||
/api/v1/files/upload:
|
/api/v1/files/upload:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -5271,6 +5332,67 @@ paths:
|
|||||||
summary: 列出目录供应商
|
summary: 列出目录供应商
|
||||||
tags:
|
tags:
|
||||||
- catalog
|
- catalog
|
||||||
|
/api/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: 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
|
||||||
/api/v1/responses:
|
/api/v1/responses:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -5847,6 +5969,67 @@ paths:
|
|||||||
summary: 创建或执行 AI 任务
|
summary: 创建或执行 AI 任务
|
||||||
tags:
|
tags:
|
||||||
- tasks
|
- tasks
|
||||||
|
/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: 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:
|
/healthz:
|
||||||
get:
|
get:
|
||||||
description: 返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。
|
description: 返回服务进程、运行环境和身份模式,供负载均衡或人工排障使用。
|
||||||
@@ -5999,6 +6182,67 @@ paths:
|
|||||||
summary: 就绪检查
|
summary: 就绪检查
|
||||||
tags:
|
tags:
|
||||||
- system
|
- 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: 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:
|
/responses:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -6191,6 +6435,67 @@ paths:
|
|||||||
summary: 创建或执行 AI 任务
|
summary: 创建或执行 AI 任务
|
||||||
tags:
|
tags:
|
||||||
- tasks
|
- tasks
|
||||||
|
/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: 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:
|
/v1/files/upload:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -6357,6 +6662,67 @@ paths:
|
|||||||
summary: 创建或执行 AI 任务
|
summary: 创建或执行 AI 任务
|
||||||
tags:
|
tags:
|
||||||
- tasks
|
- 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: 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:
|
/v1/responses:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -151,6 +151,107 @@ func TestOpenAIClientChatContract(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenAIClientEmbeddingsContract(t *testing.T) {
|
||||||
|
var gotPath string
|
||||||
|
var gotModel string
|
||||||
|
var gotDimensions float64
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatalf("decode request: %v", err)
|
||||||
|
}
|
||||||
|
gotModel, _ = body["model"].(string)
|
||||||
|
gotDimensions, _ = body["dimensions"].(float64)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": "embd-test",
|
||||||
|
"object": "list",
|
||||||
|
"model": gotModel,
|
||||||
|
"data": []any{map[string]any{
|
||||||
|
"object": "embedding",
|
||||||
|
"index": 0,
|
||||||
|
"embedding": []any{0.1, 0.2, 0.3},
|
||||||
|
}},
|
||||||
|
"usage": map[string]any{"prompt_tokens": 3, "total_tokens": 3},
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
response, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
|
||||||
|
Kind: "embeddings",
|
||||||
|
Model: "aliyun-bailian-openai:text-embedding-v4",
|
||||||
|
Body: map[string]any{
|
||||||
|
"model": "aliyun-bailian-openai:text-embedding-v4",
|
||||||
|
"input": []any{"hello"},
|
||||||
|
"dimensions": 3,
|
||||||
|
},
|
||||||
|
Candidate: store.RuntimeModelCandidate{
|
||||||
|
BaseURL: server.URL,
|
||||||
|
ProviderModelName: "text-embedding-v4",
|
||||||
|
Credentials: map[string]any{"apiKey": "test-key"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run embeddings client: %v", err)
|
||||||
|
}
|
||||||
|
if gotPath != "/embeddings" || gotModel != "text-embedding-v4" || gotDimensions != 3 {
|
||||||
|
t.Fatalf("unexpected embeddings request path=%s model=%s dimensions=%v", gotPath, gotModel, gotDimensions)
|
||||||
|
}
|
||||||
|
if response.Usage.InputTokens != 3 || response.Usage.TotalTokens != 3 || response.Result["id"] != "embd-test" {
|
||||||
|
t.Fatalf("unexpected embeddings response: %+v", response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAIClientAliyunRerankUsesCompatibleAPIBase(t *testing.T) {
|
||||||
|
var gotPath string
|
||||||
|
var gotModel string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatalf("decode request: %v", err)
|
||||||
|
}
|
||||||
|
gotModel, _ = body["model"].(string)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": "rerank-test",
|
||||||
|
"object": "list",
|
||||||
|
"model": gotModel,
|
||||||
|
"results": []any{
|
||||||
|
map[string]any{"index": 0, "relevance_score": 0.93},
|
||||||
|
map[string]any{"index": 2, "relevance_score": 0.34},
|
||||||
|
},
|
||||||
|
"usage": map[string]any{"total_tokens": 9},
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
response, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
|
||||||
|
Kind: "reranks",
|
||||||
|
Model: "aliyun-bailian-openai:qwen3-rerank",
|
||||||
|
Body: map[string]any{
|
||||||
|
"model": "aliyun-bailian-openai:qwen3-rerank",
|
||||||
|
"query": "what is rerank",
|
||||||
|
"documents": []any{"rerank sorts documents", "unrelated"},
|
||||||
|
"top_n": 2,
|
||||||
|
},
|
||||||
|
Candidate: store.RuntimeModelCandidate{
|
||||||
|
Provider: "aliyun-bailian-openai",
|
||||||
|
BaseURL: server.URL + "/compatible-mode/v1",
|
||||||
|
ProviderModelName: "qwen3-rerank",
|
||||||
|
Credentials: map[string]any{"apiKey": "test-key"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run rerank client: %v", err)
|
||||||
|
}
|
||||||
|
if gotPath != "/compatible-api/v1/reranks" || gotModel != "qwen3-rerank" {
|
||||||
|
t.Fatalf("unexpected rerank request path=%s model=%s", gotPath, gotModel)
|
||||||
|
}
|
||||||
|
if response.Usage.TotalTokens != 9 || response.Result["id"] != "rerank-test" {
|
||||||
|
t.Fatalf("unexpected rerank response: %+v", response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOpenAIClientChatRequestNormalizesToolContext(t *testing.T) {
|
func TestOpenAIClientChatRequestNormalizesToolContext(t *testing.T) {
|
||||||
var captured map[string]any
|
var captured map[string]any
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenAIClient struct {
|
type OpenAIClient struct {
|
||||||
@@ -27,10 +29,10 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
|
|||||||
body = NormalizeChatCompletionRequestBody(body)
|
body = NormalizeChatCompletionRequestBody(body)
|
||||||
}
|
}
|
||||||
body["model"] = upstreamModelName(request.Candidate)
|
body["model"] = upstreamModelName(request.Candidate)
|
||||||
stream := request.Stream || boolValue(body, "stream")
|
stream := openAIEndpointSupportsStream(request.Kind) && (request.Stream || boolValue(body, "stream"))
|
||||||
ensureOpenAIStreamUsage(body, request.Kind, stream)
|
ensureOpenAIStreamUsage(body, request.Kind, stream)
|
||||||
raw, _ := json.Marshal(body)
|
raw, _ := json.Marshal(body)
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(request.Candidate.BaseURL, endpoint), bytes.NewReader(raw))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(openAIBaseURL(request.Kind, request.Candidate), endpoint), bytes.NewReader(raw))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Response{}, err
|
return Response{}, err
|
||||||
}
|
}
|
||||||
@@ -81,6 +83,10 @@ func openAIEndpoint(kind string) string {
|
|||||||
return "/chat/completions"
|
return "/chat/completions"
|
||||||
case "responses":
|
case "responses":
|
||||||
return "/responses"
|
return "/responses"
|
||||||
|
case "embeddings":
|
||||||
|
return "/embeddings"
|
||||||
|
case "reranks":
|
||||||
|
return "/reranks"
|
||||||
case "images.generations":
|
case "images.generations":
|
||||||
return "/images/generations"
|
return "/images/generations"
|
||||||
case "images.edits":
|
case "images.edits":
|
||||||
@@ -90,6 +96,24 @@ func openAIEndpoint(kind string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openAIEndpointSupportsStream(kind string) bool {
|
||||||
|
return kind == "chat.completions" || kind == "responses"
|
||||||
|
}
|
||||||
|
|
||||||
|
func openAIBaseURL(kind string, candidate store.RuntimeModelCandidate) string {
|
||||||
|
base := strings.TrimSpace(candidate.BaseURL)
|
||||||
|
if kind != "reranks" {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
if strings.Contains(base, "/compatible-mode/") && (strings.EqualFold(candidate.Provider, "aliyun-bailian-openai") || strings.Contains(base, "dashscope")) {
|
||||||
|
return strings.Replace(base, "/compatible-mode/", "/compatible-api/", 1)
|
||||||
|
}
|
||||||
|
if base == "" && strings.EqualFold(candidate.Provider, "aliyun-bailian-openai") {
|
||||||
|
return "https://dashscope.aliyuncs.com/compatible-api/v1"
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
func cloneBody(body map[string]any) map[string]any {
|
func cloneBody(body map[string]any) map[string]any {
|
||||||
out := map[string]any{}
|
out := map[string]any{}
|
||||||
for key, value := range body {
|
for key, value := range body {
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ func simulatedResult(request Request) map[string]any {
|
|||||||
"output_text": fmt.Sprintf("simulation response from %s", request.Candidate.Provider),
|
"output_text": fmt.Sprintf("simulation response from %s", request.Candidate.Provider),
|
||||||
"usage": map[string]any{"input_tokens": 12, "output_tokens": 8, "total_tokens": 20},
|
"usage": map[string]any{"input_tokens": 12, "output_tokens": 8, "total_tokens": 20},
|
||||||
}
|
}
|
||||||
|
case "embeddings":
|
||||||
|
return simulatedEmbeddingResult(request)
|
||||||
|
case "reranks":
|
||||||
|
return simulatedRerankResult(request)
|
||||||
case "images.edits":
|
case "images.edits":
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"id": "img-edit-simulated",
|
"id": "img-edit-simulated",
|
||||||
@@ -172,6 +176,106 @@ func simulatedResult(request Request) map[string]any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func simulatedEmbeddingResult(request Request) map[string]any {
|
||||||
|
inputCount := simulatedEmbeddingInputCount(request.Body["input"])
|
||||||
|
dimensions := intValue(request.Body, "dimensions", 3)
|
||||||
|
if dimensions <= 0 {
|
||||||
|
dimensions = 3
|
||||||
|
}
|
||||||
|
if dimensions > 2048 {
|
||||||
|
dimensions = 2048
|
||||||
|
}
|
||||||
|
data := make([]any, 0, inputCount)
|
||||||
|
for index := 0; index < inputCount; index += 1 {
|
||||||
|
embedding := make([]any, 0, dimensions)
|
||||||
|
for dimension := 0; dimension < dimensions; dimension += 1 {
|
||||||
|
embedding = append(embedding, float64(index+1)/10+float64(dimension)/100)
|
||||||
|
}
|
||||||
|
data = append(data, map[string]any{
|
||||||
|
"object": "embedding",
|
||||||
|
"index": index,
|
||||||
|
"embedding": embedding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
usage := simulatedUsage(request)
|
||||||
|
return map[string]any{
|
||||||
|
"id": "embd-simulated",
|
||||||
|
"object": "list",
|
||||||
|
"model": request.Model,
|
||||||
|
"data": data,
|
||||||
|
"usage": map[string]any{"prompt_tokens": usage.InputTokens, "total_tokens": usage.TotalTokens},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func simulatedEmbeddingInputCount(value any) int {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case []any:
|
||||||
|
if len(typed) > 0 {
|
||||||
|
return len(typed)
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
if len(typed) > 0 {
|
||||||
|
return len(typed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func simulatedRerankResult(request Request) map[string]any {
|
||||||
|
documents := simulatedRerankDocuments(request.Body["documents"])
|
||||||
|
topN := intValue(request.Body, "top_n", len(documents))
|
||||||
|
if topN <= 0 || topN > len(documents) {
|
||||||
|
topN = len(documents)
|
||||||
|
}
|
||||||
|
results := make([]any, 0, topN)
|
||||||
|
for index := 0; index < topN; index += 1 {
|
||||||
|
score := 0.95 - float64(index)*0.1
|
||||||
|
if score < 0 {
|
||||||
|
score = 0
|
||||||
|
}
|
||||||
|
result := map[string]any{
|
||||||
|
"index": index,
|
||||||
|
"relevance_score": score,
|
||||||
|
}
|
||||||
|
if boolValue(request.Body, "return_documents") {
|
||||||
|
result["document"] = map[string]any{"text": documents[index]}
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
usage := simulatedUsage(request)
|
||||||
|
return map[string]any{
|
||||||
|
"id": "rerank-simulated",
|
||||||
|
"object": "list",
|
||||||
|
"model": request.Model,
|
||||||
|
"results": results,
|
||||||
|
"usage": map[string]any{"total_tokens": usage.TotalTokens},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func simulatedRerankDocuments(value any) []string {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case []any:
|
||||||
|
out := make([]string, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
text := stringValue(map[string]any{"value": item}, "value")
|
||||||
|
if text == "" {
|
||||||
|
if record, ok := item.(map[string]any); ok {
|
||||||
|
text = firstNonEmptyString(stringValue(record, "text"), stringValue(record, "content"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, text)
|
||||||
|
}
|
||||||
|
if len(out) > 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
if len(typed) > 0 {
|
||||||
|
return typed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []string{"simulated document"}
|
||||||
|
}
|
||||||
|
|
||||||
func simulatedImageData(request Request, url string, fallbackPrompt string) []any {
|
func simulatedImageData(request Request, url string, fallbackPrompt string) []any {
|
||||||
count := simulatedOutputCount(request.Body)
|
count := simulatedOutputCount(request.Body)
|
||||||
items := make([]any, 0, count)
|
items := make([]any, 0, count)
|
||||||
@@ -207,6 +311,9 @@ func simulatedUsage(request Request) Usage {
|
|||||||
if request.ModelType == "chat" || request.ModelType == "text_generate" || request.Kind == "responses" {
|
if request.ModelType == "chat" || request.ModelType == "text_generate" || request.Kind == "responses" {
|
||||||
return Usage{InputTokens: 12, OutputTokens: 8, TotalTokens: 20}
|
return Usage{InputTokens: 12, OutputTokens: 8, TotalTokens: 20}
|
||||||
}
|
}
|
||||||
|
if request.ModelType == "text_embedding" || request.ModelType == "text_rerank" || request.Kind == "embeddings" || request.Kind == "reranks" {
|
||||||
|
return Usage{InputTokens: 16, TotalTokens: 16}
|
||||||
|
}
|
||||||
return Usage{}
|
return Usage{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,33 @@ func TestPlanTaskResponseTreatsAPIV1ChatCompletionsAsSynchronousCompatibleRespon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlanTaskResponseTreatsAPIV1EmbeddingAndRerankAsSynchronousCompatibleResponse(t *testing.T) {
|
||||||
|
for _, item := range []struct {
|
||||||
|
kind string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{kind: "embeddings", path: "/api/v1/embeddings"},
|
||||||
|
{kind: "reranks", path: "/api/v1/reranks"},
|
||||||
|
} {
|
||||||
|
t.Run(item.kind, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, item.path, nil)
|
||||||
|
req.Header.Set("X-Async", "true")
|
||||||
|
|
||||||
|
plan := planTaskResponse(item.kind, false, map[string]any{"stream": true}, req)
|
||||||
|
|
||||||
|
if plan.asyncMode {
|
||||||
|
t.Fatalf("%s must not enter async task mode", item.path)
|
||||||
|
}
|
||||||
|
if !plan.compatibleMode {
|
||||||
|
t.Fatalf("%s should return compatible response payloads", item.path)
|
||||||
|
}
|
||||||
|
if plan.streamMode {
|
||||||
|
t.Fatal("embedding and rerank endpoints should stay JSON-only even when stream=true is present")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlanTaskResponseKeepsAsyncTaskModeForOtherAPIV1Tasks(t *testing.T) {
|
func TestPlanTaskResponseKeepsAsyncTaskModeForOtherAPIV1Tasks(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil)
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil)
|
||||||
req.Header.Set("X-Async", "true")
|
req.Header.Set("X-Async", "true")
|
||||||
|
|||||||
@@ -876,6 +876,8 @@ func (s *Server) listModelRateLimitStatuses(w http.ResponseWriter, r *http.Reque
|
|||||||
// @Failure 429 {object} ErrorEnvelope
|
// @Failure 429 {object} ErrorEnvelope
|
||||||
// @Failure 502 {object} ErrorEnvelope
|
// @Failure 502 {object} ErrorEnvelope
|
||||||
// @Router /api/v1/responses [post]
|
// @Router /api/v1/responses [post]
|
||||||
|
// @Router /api/v1/embeddings [post]
|
||||||
|
// @Router /api/v1/reranks [post]
|
||||||
// @Router /api/v1/images/generations [post]
|
// @Router /api/v1/images/generations [post]
|
||||||
// @Router /api/v1/images/edits [post]
|
// @Router /api/v1/images/edits [post]
|
||||||
// @Router /api/v1/videos/generations [post]
|
// @Router /api/v1/videos/generations [post]
|
||||||
@@ -883,6 +885,10 @@ func (s *Server) listModelRateLimitStatuses(w http.ResponseWriter, r *http.Reque
|
|||||||
// @Router /v1/chat/completions [post]
|
// @Router /v1/chat/completions [post]
|
||||||
// @Router /responses [post]
|
// @Router /responses [post]
|
||||||
// @Router /v1/responses [post]
|
// @Router /v1/responses [post]
|
||||||
|
// @Router /embeddings [post]
|
||||||
|
// @Router /v1/embeddings [post]
|
||||||
|
// @Router /reranks [post]
|
||||||
|
// @Router /v1/reranks [post]
|
||||||
// @Router /images/generations [post]
|
// @Router /images/generations [post]
|
||||||
// @Router /v1/images/generations [post]
|
// @Router /v1/images/generations [post]
|
||||||
// @Router /images/edits [post]
|
// @Router /images/edits [post]
|
||||||
@@ -1085,17 +1091,25 @@ type taskResponsePlan struct {
|
|||||||
func planTaskResponse(kind string, compatible bool, body map[string]any, r *http.Request) taskResponsePlan {
|
func planTaskResponse(kind string, compatible bool, body map[string]any, r *http.Request) taskResponsePlan {
|
||||||
asyncMode := asyncRequest(r)
|
asyncMode := asyncRequest(r)
|
||||||
compatibleMode := compatible
|
compatibleMode := compatible
|
||||||
if kind == "chat.completions" && !compatible {
|
if synchronousCompatibleKind(kind) && !compatible {
|
||||||
asyncMode = false
|
asyncMode = false
|
||||||
compatibleMode = true
|
compatibleMode = true
|
||||||
}
|
}
|
||||||
return taskResponsePlan{
|
return taskResponsePlan{
|
||||||
asyncMode: asyncMode,
|
asyncMode: asyncMode,
|
||||||
compatibleMode: compatibleMode,
|
compatibleMode: compatibleMode,
|
||||||
streamMode: boolValue(body, "stream"),
|
streamMode: streamCompatibleKind(kind) && boolValue(body, "stream"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func synchronousCompatibleKind(kind string) bool {
|
||||||
|
return kind == "chat.completions" || kind == "embeddings" || kind == "reranks"
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamCompatibleKind(kind string) bool {
|
||||||
|
return kind == "chat.completions" || kind == "responses"
|
||||||
|
}
|
||||||
|
|
||||||
func writeTaskAccepted(w http.ResponseWriter, task store.GatewayTask) {
|
func writeTaskAccepted(w http.ResponseWriter, task store.GatewayTask) {
|
||||||
writeJSON(w, http.StatusAccepted, map[string]any{
|
writeJSON(w, http.StatusAccepted, map[string]any{
|
||||||
"taskId": task.ID,
|
"taskId": task.ID,
|
||||||
@@ -1120,6 +1134,12 @@ func apiKeyScopeAllowed(user *auth.User, kind string) bool {
|
|||||||
if required == "chat" && (scope == "text" || scope == "text_generate") {
|
if required == "chat" && (scope == "text" || scope == "text_generate") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if required == "embedding" && scope == "text_embedding" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if required == "rerank" && scope == "text_rerank" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1128,6 +1148,10 @@ func scopeForTaskKind(kind string) string {
|
|||||||
switch kind {
|
switch kind {
|
||||||
case "chat.completions", "responses":
|
case "chat.completions", "responses":
|
||||||
return "chat"
|
return "chat"
|
||||||
|
case "embeddings":
|
||||||
|
return "embedding"
|
||||||
|
case "reranks":
|
||||||
|
return "rerank"
|
||||||
case "images.generations", "images.edits":
|
case "images.generations", "images.edits":
|
||||||
return "image"
|
return "image"
|
||||||
case "videos.generations":
|
case "videos.generations":
|
||||||
|
|||||||
@@ -1025,6 +1025,7 @@ func modelCatalogCapabilityDefinitions() []ModelCatalogFilterOption {
|
|||||||
{Value: "text_to_speech", Label: "语音合成"},
|
{Value: "text_to_speech", Label: "语音合成"},
|
||||||
{Value: "audio_understanding", Label: "音频理解"},
|
{Value: "audio_understanding", Label: "音频理解"},
|
||||||
{Value: "text_embedding", Label: "Embedding"},
|
{Value: "text_embedding", Label: "Embedding"},
|
||||||
|
{Value: "text_rerank", Label: "重排序"},
|
||||||
{Value: "omni", Label: "全模态"},
|
{Value: "omni", Label: "全模态"},
|
||||||
{Value: "omni_video", Label: "全模态视频"},
|
{Value: "omni_video", Label: "全模态视频"},
|
||||||
{Value: "multimodal", Label: "多模态"},
|
{Value: "multimodal", Label: "多模态"},
|
||||||
@@ -1120,6 +1121,8 @@ func canonicalCapabilityFilterValue(value string) string {
|
|||||||
switch normalized {
|
switch normalized {
|
||||||
case "embedding":
|
case "embedding":
|
||||||
return "text_embedding"
|
return "text_embedding"
|
||||||
|
case "rerank", "reranks":
|
||||||
|
return "text_rerank"
|
||||||
case "model":
|
case "model":
|
||||||
return "model_3d"
|
return "model_3d"
|
||||||
default:
|
default:
|
||||||
@@ -1143,6 +1146,8 @@ func capabilityFilterValueForTag(tag string) string {
|
|||||||
return "structured_output"
|
return "structured_output"
|
||||||
case "数字人":
|
case "数字人":
|
||||||
return "digital_human"
|
return "digital_human"
|
||||||
|
case "重排序":
|
||||||
|
return "text_rerank"
|
||||||
case "3D 模型":
|
case "3D 模型":
|
||||||
return "model_3d"
|
return "model_3d"
|
||||||
case "文生 3D":
|
case "文生 3D":
|
||||||
@@ -1165,6 +1170,9 @@ func capabilityLabel(value string) string {
|
|||||||
"responses": "Responses",
|
"responses": "Responses",
|
||||||
"text_embedding": "Embedding",
|
"text_embedding": "Embedding",
|
||||||
"embedding": "Embedding",
|
"embedding": "Embedding",
|
||||||
|
"text_rerank": "重排序",
|
||||||
|
"rerank": "重排序",
|
||||||
|
"reranks": "重排序",
|
||||||
"image_generate": "图像生成",
|
"image_generate": "图像生成",
|
||||||
"image_edit": "图像编辑",
|
"image_edit": "图像编辑",
|
||||||
"image_analysis": "图像分析",
|
"image_analysis": "图像分析",
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
|||||||
mux.Handle("GET /api/admin/runtime/model-rate-limits", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listModelRateLimitStatuses)))
|
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.auth.Require(auth.PermissionBasic, server.createAPIV1ChatCompletions()))
|
mux.Handle("POST /api/v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createAPIV1ChatCompletions()))
|
||||||
mux.Handle("POST /api/v1/responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", false)))
|
mux.Handle("POST /api/v1/responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", false)))
|
||||||
|
mux.Handle("POST /api/v1/embeddings", server.auth.Require(auth.PermissionBasic, server.createTask("embeddings", false)))
|
||||||
|
mux.Handle("POST /api/v1/reranks", server.auth.Require(auth.PermissionBasic, server.createTask("reranks", false)))
|
||||||
mux.Handle("POST /api/v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", false)))
|
mux.Handle("POST /api/v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", false)))
|
||||||
mux.Handle("POST /api/v1/images/edits", server.auth.Require(auth.PermissionBasic, server.createTask("images.edits", false)))
|
mux.Handle("POST /api/v1/images/edits", server.auth.Require(auth.PermissionBasic, server.createTask("images.edits", false)))
|
||||||
mux.Handle("POST /api/v1/videos/generations", server.auth.Require(auth.PermissionBasic, server.createTask("videos.generations", false)))
|
mux.Handle("POST /api/v1/videos/generations", server.auth.Require(auth.PermissionBasic, server.createTask("videos.generations", false)))
|
||||||
@@ -140,6 +142,10 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
|||||||
mux.Handle("POST /v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions", true)))
|
mux.Handle("POST /v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions", true)))
|
||||||
mux.Handle("POST /responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", true)))
|
mux.Handle("POST /responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", true)))
|
||||||
mux.Handle("POST /v1/responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", true)))
|
mux.Handle("POST /v1/responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", true)))
|
||||||
|
mux.Handle("POST /embeddings", server.auth.Require(auth.PermissionBasic, server.createTask("embeddings", true)))
|
||||||
|
mux.Handle("POST /v1/embeddings", server.auth.Require(auth.PermissionBasic, server.createTask("embeddings", true)))
|
||||||
|
mux.Handle("POST /reranks", server.auth.Require(auth.PermissionBasic, server.createTask("reranks", true)))
|
||||||
|
mux.Handle("POST /v1/reranks", server.auth.Require(auth.PermissionBasic, server.createTask("reranks", true)))
|
||||||
mux.Handle("POST /images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", true)))
|
mux.Handle("POST /images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", true)))
|
||||||
mux.Handle("POST /v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", true)))
|
mux.Handle("POST /v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", true)))
|
||||||
mux.Handle("POST /images/edits", server.auth.Require(auth.PermissionBasic, server.createTask("images.edits", true)))
|
mux.Handle("POST /images/edits", server.auth.Require(auth.PermissionBasic, server.createTask("images.edits", true)))
|
||||||
|
|||||||
@@ -159,16 +159,14 @@ func hasRules(policy map[string]any) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func estimateRequestTokens(body map[string]any) int {
|
func estimateRequestTokens(body map[string]any) int {
|
||||||
text := ""
|
var text strings.Builder
|
||||||
if prompt := stringFromMap(body, "prompt"); prompt != "" {
|
appendTokenEstimateText(&text, body["prompt"])
|
||||||
text += prompt
|
appendTokenEstimateText(&text, body["input"])
|
||||||
}
|
appendTokenEstimateText(&text, body["query"])
|
||||||
if input := stringFromMap(body, "input"); input != "" {
|
appendTokenEstimateText(&text, body["documents"])
|
||||||
text += input
|
|
||||||
}
|
|
||||||
for _, item := range contentItems(body["content"]) {
|
for _, item := range contentItems(body["content"]) {
|
||||||
if stringFromAny(item["type"]) == "text" {
|
if stringFromAny(item["type"]) == "text" {
|
||||||
text += stringFromAny(item["text"])
|
appendTokenEstimateText(&text, item["text"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if messages, ok := body["messages"].([]any); ok {
|
if messages, ok := body["messages"].([]any); ok {
|
||||||
@@ -176,19 +174,41 @@ func estimateRequestTokens(body map[string]any) int {
|
|||||||
message, _ := raw.(map[string]any)
|
message, _ := raw.(map[string]any)
|
||||||
switch content := message["content"].(type) {
|
switch content := message["content"].(type) {
|
||||||
case string:
|
case string:
|
||||||
text += content
|
appendTokenEstimateText(&text, content)
|
||||||
case []any:
|
case []any:
|
||||||
for _, rawPart := range content {
|
for _, rawPart := range content {
|
||||||
part, _ := rawPart.(map[string]any)
|
part, _ := rawPart.(map[string]any)
|
||||||
text += stringFromMap(part, "text")
|
appendTokenEstimateText(&text, part["text"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if text == "" {
|
estimatedText := text.String()
|
||||||
|
if estimatedText == "" {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return len([]rune(text))/4 + 1
|
return len([]rune(estimatedText))/4 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendTokenEstimateText(out *strings.Builder, value any) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case string:
|
||||||
|
out.WriteString(typed)
|
||||||
|
case []string:
|
||||||
|
for _, item := range typed {
|
||||||
|
out.WriteString(item)
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, item := range typed {
|
||||||
|
appendTokenEstimateText(out, item)
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
for _, key := range []string{"text", "content", "query", "document"} {
|
||||||
|
if text := stringFromAny(typed[key]); text != "" {
|
||||||
|
out.WriteString(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenUsageAmounts(usage clients.Usage) map[string]float64 {
|
func tokenUsageAmounts(usage clients.Usage) map[string]float64 {
|
||||||
|
|||||||
@@ -40,8 +40,11 @@ func (s *Service) Estimate(ctx context.Context, kind string, model string, body
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) estimatedBillings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate) []any {
|
func (s *Service) estimatedBillings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate) []any {
|
||||||
usage := clients.Usage{InputTokens: estimateRequestTokens(body), OutputTokens: int(floatFromAny(body["max_tokens"]))}
|
usage := clients.Usage{InputTokens: estimateRequestTokens(body)}
|
||||||
if usage.OutputTokens == 0 {
|
if isTextGenerationKind(kind) {
|
||||||
|
usage.OutputTokens = int(floatFromAny(body["max_tokens"]))
|
||||||
|
}
|
||||||
|
if isTextGenerationKind(kind) && usage.OutputTokens == 0 {
|
||||||
usage.OutputTokens = 64
|
usage.OutputTokens = 64
|
||||||
}
|
}
|
||||||
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
|
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
|
||||||
@@ -56,19 +59,25 @@ func (s *Service) estimatedBillings(ctx context.Context, user *auth.User, kind s
|
|||||||
func (s *Service) billings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) []any {
|
func (s *Service) billings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) []any {
|
||||||
config := s.effectiveBillingConfig(ctx, candidate)
|
config := s.effectiveBillingConfig(ctx, candidate)
|
||||||
discount := effectiveDiscount(ctx, s.store, user, candidate)
|
discount := effectiveDiscount(ctx, s.store, user, candidate)
|
||||||
if isTextGenerationKind(kind) {
|
if isTextBillingKind(kind) {
|
||||||
inputTokens := response.Usage.InputTokens
|
inputTokens := response.Usage.InputTokens
|
||||||
outputTokens := response.Usage.OutputTokens
|
outputTokens := response.Usage.OutputTokens
|
||||||
|
if isTextInputOnlyKind(kind) && inputTokens == 0 && response.Usage.TotalTokens > 0 {
|
||||||
|
inputTokens = response.Usage.TotalTokens
|
||||||
|
}
|
||||||
if inputTokens == 0 && outputTokens == 0 {
|
if inputTokens == 0 && outputTokens == 0 {
|
||||||
inputTokens = estimateRequestTokens(body)
|
inputTokens = estimateRequestTokens(body)
|
||||||
outputTokens = 1
|
if isTextGenerationKind(kind) {
|
||||||
|
outputTokens = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inputAmount := roundPrice(float64(inputTokens) / 1000 * resourcePrice(config, "text", "textInputPer1k", "inputTokenPrice", "basePrice") * discount)
|
inputAmount := roundPrice(float64(inputTokens) / 1000 * resourcePrice(config, "text", "textInputPer1k", "inputTokenPrice", "basePrice") * discount)
|
||||||
outputAmount := roundPrice(float64(outputTokens) / 1000 * resourcePrice(config, "text", "textOutputPer1k", "outputTokenPrice", "basePrice") * discount)
|
lines := []any{billingLine(candidate, "text_input", "1k_tokens", inputTokens, inputAmount, discount, simulated)}
|
||||||
return []any{
|
if isTextGenerationKind(kind) {
|
||||||
billingLine(candidate, "text_input", "1k_tokens", inputTokens, inputAmount, discount, simulated),
|
outputAmount := roundPrice(float64(outputTokens) / 1000 * resourcePrice(config, "text", "textOutputPer1k", "outputTokenPrice", "basePrice") * discount)
|
||||||
billingLine(candidate, "text_output", "1k_tokens", outputTokens, outputAmount, discount, simulated),
|
lines = append(lines, billingLine(candidate, "text_output", "1k_tokens", outputTokens, outputAmount, discount, simulated))
|
||||||
}
|
}
|
||||||
|
return lines
|
||||||
}
|
}
|
||||||
count := requestOutputCount(body)
|
count := requestOutputCount(body)
|
||||||
resource := "image"
|
resource := "image"
|
||||||
|
|||||||
@@ -698,7 +698,7 @@ func (s *Service) recordTaskParameterPreprocessing(ctx context.Context, taskID s
|
|||||||
|
|
||||||
func skipTaskParameterPreprocessingLog(modelType string) bool {
|
func skipTaskParameterPreprocessingLog(modelType string) bool {
|
||||||
switch strings.TrimSpace(modelType) {
|
switch strings.TrimSpace(modelType) {
|
||||||
case "text_generate", "chat", "responses", "text":
|
case "text_generate", "text_embedding", "text_rerank", "chat", "responses", "text":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -923,6 +923,10 @@ func modelTypeFromKind(kind string, body map[string]any) string {
|
|||||||
switch kind {
|
switch kind {
|
||||||
case "chat.completions", "responses":
|
case "chat.completions", "responses":
|
||||||
return "text_generate"
|
return "text_generate"
|
||||||
|
case "embeddings":
|
||||||
|
return "text_embedding"
|
||||||
|
case "reranks":
|
||||||
|
return "text_rerank"
|
||||||
case "images.generations", "images.edits":
|
case "images.generations", "images.edits":
|
||||||
if kind == "images.edits" {
|
if kind == "images.edits" {
|
||||||
return "image_edit"
|
return "image_edit"
|
||||||
@@ -943,7 +947,7 @@ func modelTypeFromKind(kind string, body map[string]any) string {
|
|||||||
|
|
||||||
func requestedModelTypeFromBody(body map[string]any) string {
|
func requestedModelTypeFromBody(body map[string]any) string {
|
||||||
for _, key := range []string{"modelType", "model_type", "capability", "capabilityType"} {
|
for _, key := range []string{"modelType", "model_type", "capability", "capabilityType"} {
|
||||||
value := strings.TrimSpace(stringFromMap(body, key))
|
value := canonicalModelType(strings.TrimSpace(stringFromMap(body, key)))
|
||||||
if isKnownModelType(value) {
|
if isKnownModelType(value) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
@@ -951,9 +955,21 @@ func requestedModelTypeFromBody(body map[string]any) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canonicalModelType(value string) string {
|
||||||
|
normalized := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(value)), "-", "_")
|
||||||
|
switch normalized {
|
||||||
|
case "embedding":
|
||||||
|
return "text_embedding"
|
||||||
|
case "rerank", "reranks":
|
||||||
|
return "text_rerank"
|
||||||
|
default:
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isKnownModelType(value string) bool {
|
func isKnownModelType(value string) bool {
|
||||||
switch value {
|
switch value {
|
||||||
case "text_generate", "image_generate", "image_edit", "video_generate", "image_to_video", "text_to_video", "video_edit", "video_reference", "video_first_last_frame", "omni_video", "omni":
|
case "text_generate", "text_embedding", "text_rerank", "image_generate", "image_edit", "video_generate", "image_to_video", "text_to_video", "video_edit", "video_reference", "video_first_last_frame", "omni_video", "omni":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -1001,6 +1017,14 @@ func isTextGenerationKind(kind string) bool {
|
|||||||
return kind == "chat.completions" || kind == "responses"
|
return kind == "chat.completions" || kind == "responses"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isTextInputOnlyKind(kind string) bool {
|
||||||
|
return kind == "embeddings" || kind == "reranks"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTextBillingKind(kind string) bool {
|
||||||
|
return isTextGenerationKind(kind) || isTextInputOnlyKind(kind)
|
||||||
|
}
|
||||||
|
|
||||||
func isSimulation(task store.GatewayTask, candidate store.RuntimeModelCandidate) bool {
|
func isSimulation(task store.GatewayTask, candidate store.RuntimeModelCandidate) bool {
|
||||||
if task.RunMode == "simulation" {
|
if task.RunMode == "simulation" {
|
||||||
return true
|
return true
|
||||||
@@ -1115,6 +1139,17 @@ func validateRequest(kind string, body map[string]any) error {
|
|||||||
if body["input"] == nil && body["messages"] == nil {
|
if body["input"] == nil && body["messages"] == nil {
|
||||||
return errors.New("input or messages is required")
|
return errors.New("input or messages is required")
|
||||||
}
|
}
|
||||||
|
case "embeddings":
|
||||||
|
if body["input"] == nil {
|
||||||
|
return errors.New("input is required")
|
||||||
|
}
|
||||||
|
case "reranks":
|
||||||
|
if body["query"] == nil {
|
||||||
|
return errors.New("query is required")
|
||||||
|
}
|
||||||
|
if !hasRerankDocuments(body["documents"]) {
|
||||||
|
return errors.New("documents is required")
|
||||||
|
}
|
||||||
case "images.generations", "images.edits":
|
case "images.generations", "images.edits":
|
||||||
if strings.TrimSpace(stringFromMap(body, "prompt")) == "" {
|
if strings.TrimSpace(stringFromMap(body, "prompt")) == "" {
|
||||||
return errors.New("prompt is required")
|
return errors.New("prompt is required")
|
||||||
@@ -1123,6 +1158,17 @@ func validateRequest(kind string, body map[string]any) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasRerankDocuments(value any) bool {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case []any:
|
||||||
|
return len(typed) > 0
|
||||||
|
case []string:
|
||||||
|
return len(typed) > 0
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parameterPreprocessClientError(err error) *clients.ClientError {
|
func parameterPreprocessClientError(err error) *clients.ClientError {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func billingResourcesForModelTypes(modelTypes []string) map[string]bool {
|
|||||||
resources := map[string]bool{}
|
resources := map[string]bool{}
|
||||||
for _, modelType := range modelTypes {
|
for _, modelType := range modelTypes {
|
||||||
switch normalizeBillingType(modelType) {
|
switch normalizeBillingType(modelType) {
|
||||||
case "chat", "text", "responses", "text_generate", "text_embedding", "embedding",
|
case "chat", "text", "responses", "text_generate", "text_embedding", "embedding", "text_rerank", "rerank",
|
||||||
"image_analysis", "video_understanding", "audio_understanding", "omni", "tools_call":
|
"image_analysis", "video_understanding", "audio_understanding", "omni", "tools_call":
|
||||||
resources["text"] = true
|
resources["text"] = true
|
||||||
case "image", "images.generations", "image_generate":
|
case "image", "images.generations", "image_generate":
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
WITH aliyun_model_defs AS (
|
||||||
|
SELECT
|
||||||
|
'aliyun-bailian-openai' AS provider_key,
|
||||||
|
'aliyun-bailian-openai:text-embedding-v4' AS canonical_model_key,
|
||||||
|
'text-embedding-v4' AS provider_model_name,
|
||||||
|
'Qwen3-Embedding-v4' AS display_name,
|
||||||
|
'Qwen3-Embedding-v4' AS model_alias,
|
||||||
|
'Qwen3-Embedding 系列,默认维度 1024,最长输入 8192 tokens;支持 100+ 语种与多种编程语言。' AS description,
|
||||||
|
'https://static.51easyai.com/qwen-color.webp' AS icon_path,
|
||||||
|
jsonb_build_array('text_embedding') AS model_type,
|
||||||
|
jsonb_build_object(
|
||||||
|
'text_embedding', jsonb_build_object(
|
||||||
|
'dimensions', jsonb_build_array(2048, 1536, 1024, 768, 512, 256, 128, 64),
|
||||||
|
'defaultDimension', 1024,
|
||||||
|
'maxRows', 10,
|
||||||
|
'maxTokensPerRow', 8192
|
||||||
|
),
|
||||||
|
'originalTypes', jsonb_build_array('text_embedding')
|
||||||
|
) AS capabilities
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'aliyun-bailian-openai',
|
||||||
|
'aliyun-bailian-openai:text-embedding-v3',
|
||||||
|
'text-embedding-v3',
|
||||||
|
'Qwen3-Embedding-v3',
|
||||||
|
'Qwen3-Embedding-v3',
|
||||||
|
'Qwen3-Embedding 系列,默认维度 1024;支持中文、英文及 50+ 主流语种。',
|
||||||
|
'https://static.51easyai.com/qwen-color.webp',
|
||||||
|
jsonb_build_array('text_embedding'),
|
||||||
|
jsonb_build_object(
|
||||||
|
'text_embedding', jsonb_build_object(
|
||||||
|
'dimensions', jsonb_build_array(1024, 768, 512, 256, 128, 64),
|
||||||
|
'defaultDimension', 1024
|
||||||
|
),
|
||||||
|
'originalTypes', jsonb_build_array('text_embedding')
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'aliyun-bailian-openai',
|
||||||
|
'aliyun-bailian-openai:text-embedding-v2',
|
||||||
|
'text-embedding-v2',
|
||||||
|
'Text-Embedding-v2',
|
||||||
|
'Text-Embedding-v2',
|
||||||
|
'固定维度 1536,最长输入 2048 tokens;支持中英西法葡印尼日韩德俄等语种。',
|
||||||
|
'https://static.51easyai.com/qwen-color.webp',
|
||||||
|
jsonb_build_array('text_embedding'),
|
||||||
|
jsonb_build_object(
|
||||||
|
'text_embedding', jsonb_build_object(
|
||||||
|
'dimensions', jsonb_build_array(1536),
|
||||||
|
'defaultDimension', 1536,
|
||||||
|
'maxRows', 25,
|
||||||
|
'maxTokensPerRow', 2048
|
||||||
|
),
|
||||||
|
'originalTypes', jsonb_build_array('text_embedding')
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'aliyun-bailian-openai',
|
||||||
|
'aliyun-bailian-openai:text-embedding-v1',
|
||||||
|
'text-embedding-v1',
|
||||||
|
'Text-Embedding-v1',
|
||||||
|
'Text-Embedding-v1',
|
||||||
|
'百炼 OpenAI 兼容渠道 embedding 基础模型。',
|
||||||
|
'https://static.51easyai.com/qwen-color.webp',
|
||||||
|
jsonb_build_array('text_embedding'),
|
||||||
|
jsonb_build_object('originalTypes', jsonb_build_array('text_embedding'))
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'aliyun-bailian-openai',
|
||||||
|
'aliyun-bailian-openai:qwen3-rerank',
|
||||||
|
'qwen3-rerank',
|
||||||
|
'Qwen3-Rerank',
|
||||||
|
'Qwen3-Rerank',
|
||||||
|
'阿里云百炼 OpenAI 兼容重排序模型,支持 100+ 语种,适用于语义文本搜索和 RAG。',
|
||||||
|
'https://static.51easyai.com/qwen-color.webp',
|
||||||
|
jsonb_build_array('text_rerank'),
|
||||||
|
jsonb_build_object(
|
||||||
|
'text_rerank', jsonb_build_object(
|
||||||
|
'maxDocuments', 500,
|
||||||
|
'maxTokensPerDocument', 4000,
|
||||||
|
'maxRequestTokens', 120000,
|
||||||
|
'supportTopN', true,
|
||||||
|
'supportInstruct', true
|
||||||
|
),
|
||||||
|
'originalTypes', jsonb_build_array('text_rerank')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
source_rows AS (
|
||||||
|
SELECT
|
||||||
|
providers.id AS provider_id,
|
||||||
|
defs.provider_key,
|
||||||
|
defs.canonical_model_key,
|
||||||
|
defs.provider_model_name,
|
||||||
|
defs.model_type,
|
||||||
|
defs.display_name,
|
||||||
|
defs.model_alias,
|
||||||
|
defs.description,
|
||||||
|
defs.icon_path,
|
||||||
|
defs.capabilities,
|
||||||
|
COALESCE(template.base_billing_config, '{"text":{"basePrice":0.01,"baseWeight":1}}'::jsonb) AS base_billing_config,
|
||||||
|
COALESCE(template.default_rate_limit_policy, '{}'::jsonb) AS default_rate_limit_policy,
|
||||||
|
COALESCE(
|
||||||
|
template.pricing_rule_set_id,
|
||||||
|
(SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)
|
||||||
|
) AS pricing_rule_set_id,
|
||||||
|
COALESCE(
|
||||||
|
template.runtime_policy_set_id,
|
||||||
|
(SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)
|
||||||
|
) AS runtime_policy_set_id,
|
||||||
|
COALESCE(template.runtime_policy_override, '{}'::jsonb) AS runtime_policy_override,
|
||||||
|
COALESCE(template.pricing_version, 1) AS pricing_version
|
||||||
|
FROM aliyun_model_defs defs
|
||||||
|
LEFT JOIN model_catalog_providers providers
|
||||||
|
ON providers.provider_key = defs.provider_key
|
||||||
|
OR providers.provider_code = defs.provider_key
|
||||||
|
LEFT JOIN base_model_catalog template
|
||||||
|
ON template.canonical_model_key = 'aliyun-bailian-openai:text-embedding-v4'
|
||||||
|
),
|
||||||
|
payload AS (
|
||||||
|
SELECT
|
||||||
|
source_rows.*,
|
||||||
|
jsonb_build_object(
|
||||||
|
'source', 'aliyun.model-studio.docs',
|
||||||
|
'sourceProviderCode', provider_key,
|
||||||
|
'sourceProviderName', '阿里云百炼(OpenAI兼容)',
|
||||||
|
'sourceSpecType', 'openai',
|
||||||
|
'originalTypes', model_type,
|
||||||
|
'alias', model_alias,
|
||||||
|
'description', description,
|
||||||
|
'iconPath', icon_path,
|
||||||
|
'billingType', 'external-api',
|
||||||
|
'billingMode', '',
|
||||||
|
'referenceModel', '',
|
||||||
|
'modelWeight', NULL,
|
||||||
|
'selectable', true,
|
||||||
|
'rawModel', jsonb_build_object(
|
||||||
|
'name', provider_model_name,
|
||||||
|
'types', model_type,
|
||||||
|
'icon_path', icon_path,
|
||||||
|
'alias', model_alias,
|
||||||
|
'description', description,
|
||||||
|
'capabilities', capabilities - 'originalTypes'
|
||||||
|
)
|
||||||
|
) AS metadata
|
||||||
|
FROM source_rows
|
||||||
|
),
|
||||||
|
snapshot AS (
|
||||||
|
SELECT
|
||||||
|
payload.*,
|
||||||
|
jsonb_build_object(
|
||||||
|
'providerKey', provider_key,
|
||||||
|
'canonicalModelKey', canonical_model_key,
|
||||||
|
'providerModelName', provider_model_name,
|
||||||
|
'modelType', model_type,
|
||||||
|
'modelAlias', model_alias,
|
||||||
|
'capabilities', capabilities,
|
||||||
|
'baseBillingConfig', base_billing_config,
|
||||||
|
'defaultRateLimitPolicy', default_rate_limit_policy,
|
||||||
|
'pricingRuleSetId', COALESCE(pricing_rule_set_id::text, ''),
|
||||||
|
'runtimePolicySetId', COALESCE(runtime_policy_set_id::text, ''),
|
||||||
|
'runtimePolicyOverride', runtime_policy_override,
|
||||||
|
'metadata', metadata,
|
||||||
|
'pricingVersion', pricing_version,
|
||||||
|
'status', 'active'
|
||||||
|
) AS default_snapshot
|
||||||
|
FROM payload
|
||||||
|
)
|
||||||
|
INSERT INTO base_model_catalog (
|
||||||
|
provider_id,
|
||||||
|
provider_key,
|
||||||
|
canonical_model_key,
|
||||||
|
provider_model_name,
|
||||||
|
model_type,
|
||||||
|
display_name,
|
||||||
|
capabilities,
|
||||||
|
base_billing_config,
|
||||||
|
default_rate_limit_policy,
|
||||||
|
pricing_rule_set_id,
|
||||||
|
runtime_policy_set_id,
|
||||||
|
runtime_policy_override,
|
||||||
|
metadata,
|
||||||
|
catalog_type,
|
||||||
|
default_snapshot,
|
||||||
|
pricing_version,
|
||||||
|
status
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
provider_id,
|
||||||
|
provider_key,
|
||||||
|
canonical_model_key,
|
||||||
|
provider_model_name,
|
||||||
|
model_type,
|
||||||
|
display_name,
|
||||||
|
capabilities,
|
||||||
|
base_billing_config,
|
||||||
|
default_rate_limit_policy,
|
||||||
|
pricing_rule_set_id,
|
||||||
|
runtime_policy_set_id,
|
||||||
|
runtime_policy_override,
|
||||||
|
metadata,
|
||||||
|
'system',
|
||||||
|
default_snapshot,
|
||||||
|
pricing_version,
|
||||||
|
'active'
|
||||||
|
FROM snapshot
|
||||||
|
ON CONFLICT (canonical_model_key) DO UPDATE
|
||||||
|
SET provider_id = EXCLUDED.provider_id,
|
||||||
|
provider_key = EXCLUDED.provider_key,
|
||||||
|
provider_model_name = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.provider_model_name
|
||||||
|
ELSE base_model_catalog.provider_model_name
|
||||||
|
END,
|
||||||
|
model_type = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.model_type
|
||||||
|
ELSE base_model_catalog.model_type
|
||||||
|
END,
|
||||||
|
display_name = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.display_name
|
||||||
|
ELSE base_model_catalog.display_name
|
||||||
|
END,
|
||||||
|
capabilities = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.capabilities
|
||||||
|
ELSE base_model_catalog.capabilities
|
||||||
|
END,
|
||||||
|
base_billing_config = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.base_billing_config
|
||||||
|
ELSE base_model_catalog.base_billing_config
|
||||||
|
END,
|
||||||
|
default_rate_limit_policy = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.default_rate_limit_policy
|
||||||
|
ELSE base_model_catalog.default_rate_limit_policy
|
||||||
|
END,
|
||||||
|
pricing_rule_set_id = COALESCE(base_model_catalog.pricing_rule_set_id, EXCLUDED.pricing_rule_set_id),
|
||||||
|
runtime_policy_set_id = COALESCE(base_model_catalog.runtime_policy_set_id, EXCLUDED.runtime_policy_set_id),
|
||||||
|
runtime_policy_override = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.runtime_policy_override
|
||||||
|
ELSE base_model_catalog.runtime_policy_override
|
||||||
|
END,
|
||||||
|
metadata = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.metadata
|
||||||
|
ELSE base_model_catalog.metadata
|
||||||
|
END,
|
||||||
|
catalog_type = 'system',
|
||||||
|
default_snapshot = EXCLUDED.default_snapshot,
|
||||||
|
pricing_version = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN EXCLUDED.pricing_version
|
||||||
|
ELSE base_model_catalog.pricing_version
|
||||||
|
END,
|
||||||
|
status = CASE
|
||||||
|
WHEN base_model_catalog.customized_at IS NULL THEN 'active'
|
||||||
|
ELSE base_model_catalog.status
|
||||||
|
END,
|
||||||
|
updated_at = now();
|
||||||
+19
-5
@@ -540,7 +540,7 @@ export function App() {
|
|||||||
try {
|
try {
|
||||||
const response = await createApiKey(token, {
|
const response = await createApiKey(token, {
|
||||||
name: apiKeyForm.name,
|
name: apiKeyForm.name,
|
||||||
scopes: ['chat', 'image', 'video'],
|
scopes: ['chat', 'embedding', 'rerank', 'image', 'video'],
|
||||||
expiresAt: apiKeyForm.expiresAt ? new Date(apiKeyForm.expiresAt).toISOString() : undefined,
|
expiresAt: apiKeyForm.expiresAt ? new Date(apiKeyForm.expiresAt).toISOString() : undefined,
|
||||||
});
|
});
|
||||||
setApiKeySecret(response.secret);
|
setApiKeySecret(response.secret);
|
||||||
@@ -907,11 +907,20 @@ export function App() {
|
|||||||
|
|
||||||
async function submitTask(event: FormEvent<HTMLFormElement>) {
|
async function submitTask(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const credential = apiKeySecret || token;
|
const selectedApiKeySecret = selectedPlaygroundApiKeyId ? apiKeySecretsById[selectedPlaygroundApiKeyId] ?? '' : '';
|
||||||
|
const fallbackApiKeySecret = apiKeys.find((item) => Boolean(apiKeySecretsById[item.id]))?.id;
|
||||||
|
const credential = selectedApiKeySecret || (fallbackApiKeySecret ? apiKeySecretsById[fallbackApiKeySecret] : '') || apiKeySecret || token;
|
||||||
|
const credentialLabel = selectedApiKeySecret || fallbackApiKeySecret || apiKeySecret ? '本地 API Key' : '当前 Access Token';
|
||||||
setCoreState('loading');
|
setCoreState('loading');
|
||||||
setCoreMessage('');
|
setCoreMessage('');
|
||||||
try {
|
try {
|
||||||
const response = await runTask(credential, taskForm);
|
const response = await runTask(credential, taskForm);
|
||||||
|
if (response.localOnly) {
|
||||||
|
setTaskResult(response.task);
|
||||||
|
setCoreState('ready');
|
||||||
|
setCoreMessage(`${taskForm.kind} 已通过 ${credentialLabel} 完成 simulation。`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const syncTask = (detail: GatewayTask) => {
|
const syncTask = (detail: GatewayTask) => {
|
||||||
setTaskResult(detail);
|
setTaskResult(detail);
|
||||||
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
|
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
|
||||||
@@ -921,7 +930,7 @@ export function App() {
|
|||||||
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
|
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
|
||||||
invalidateDataKeys('tasks', 'wallet', 'walletTransactions');
|
invalidateDataKeys('tasks', 'wallet', 'walletTransactions');
|
||||||
setCoreState('ready');
|
setCoreState('ready');
|
||||||
setCoreMessage(`${taskForm.kind} 已通过 ${apiKeySecret ? '本地 API Key' : '当前 Access Token'} 完成 simulation。`);
|
setCoreMessage(`${taskForm.kind} 已通过 ${credentialLabel} 完成 simulation。`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setCoreState('error');
|
setCoreState('error');
|
||||||
setCoreMessage(err instanceof Error ? err.message : '测试任务失败');
|
setCoreMessage(err instanceof Error ? err.message : '测试任务失败');
|
||||||
@@ -1170,12 +1179,16 @@ export function App() {
|
|||||||
{activePage === 'docs' && (
|
{activePage === 'docs' && (
|
||||||
<ApiDocsPage
|
<ApiDocsPage
|
||||||
activeDocSection={apiDocSection}
|
activeDocSection={apiDocSection}
|
||||||
apiKeySecret={apiKeySecret}
|
apiKeySecretsById={apiKeySecretsById}
|
||||||
|
apiKeys={apiKeys}
|
||||||
canRun={isAuthenticated}
|
canRun={isAuthenticated}
|
||||||
coreMessage={coreMessage}
|
coreMessage={coreMessage}
|
||||||
coreState={coreState}
|
coreState={coreState}
|
||||||
|
selectedApiKeyId={selectedPlaygroundApiKeyId}
|
||||||
taskForm={taskForm}
|
taskForm={taskForm}
|
||||||
taskResult={taskResult}
|
taskResult={taskResult}
|
||||||
|
onApiKeyChange={setSelectedPlaygroundApiKeyId}
|
||||||
|
onCreateApiKey={openApiKeyCreation}
|
||||||
onDocSectionChange={navigateApiDocSection}
|
onDocSectionChange={navigateApiDocSection}
|
||||||
onLogin={showLogin}
|
onLogin={showLogin}
|
||||||
onSubmitTask={submitTask}
|
onSubmitTask={submitTask}
|
||||||
@@ -1318,7 +1331,8 @@ function dataKeysForRoute(
|
|||||||
? ['modelCatalog']
|
? ['modelCatalog']
|
||||||
: ['publicCatalog'];
|
: ['publicCatalog'];
|
||||||
}
|
}
|
||||||
if (activePage === 'home' || activePage === 'docs') return [];
|
if (activePage === 'docs') return isAuthenticated ? ['playgroundApiKeys'] : [];
|
||||||
|
if (activePage === 'home') return [];
|
||||||
if (!isAuthenticated) return [];
|
if (!isAuthenticated) return [];
|
||||||
|
|
||||||
if (activePage === 'workspace') {
|
if (activePage === 'workspace') {
|
||||||
|
|||||||
@@ -547,6 +547,39 @@ export async function createChatTask(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createCompatibleChatCompletion(
|
||||||
|
token: string,
|
||||||
|
input: { model: string; messages: Array<Record<string, unknown>>; runMode?: string; simulation?: boolean; stream?: boolean },
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
return request<Record<string, unknown>>('/v1/chat/completions', {
|
||||||
|
body: input,
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEmbedding(
|
||||||
|
token: string,
|
||||||
|
input: { model: string; input: string | string[]; dimensions?: number; runMode?: string; simulation?: boolean },
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
return request<Record<string, unknown>>('/v1/embeddings', {
|
||||||
|
body: input,
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRerank(
|
||||||
|
token: string,
|
||||||
|
input: { model: string; query: string; documents: string[]; top_n?: number; runMode?: string; simulation?: boolean },
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
return request<Record<string, unknown>>('/v1/reranks', {
|
||||||
|
body: input,
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function streamChatCompletions(
|
export async function streamChatCompletions(
|
||||||
token: string,
|
token: string,
|
||||||
input: { model: string; messages: Array<Record<string, unknown>>; simulation?: boolean },
|
input: { model: string; messages: Array<Record<string, unknown>>; simulation?: boolean },
|
||||||
|
|||||||
@@ -1,8 +1,45 @@
|
|||||||
import type { GatewayTask } from '@easyai-ai-gateway/contracts';
|
import type { GatewayTask } from '@easyai-ai-gateway/contracts';
|
||||||
import { createChatTask, createImageEditTask, createImageGenerationTask } from '../api';
|
import { createCompatibleChatCompletion, createEmbedding, createImageEditTask, createImageGenerationTask, createRerank } from '../api';
|
||||||
import type { TaskForm } from '../types';
|
import type { TaskForm } from '../types';
|
||||||
|
|
||||||
export function runTask(token: string, task: TaskForm): Promise<{ task: GatewayTask; next: Record<string, string> }> {
|
export interface RunTaskResponse {
|
||||||
|
localOnly?: boolean;
|
||||||
|
next?: Record<string, string>;
|
||||||
|
task: GatewayTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runTask(token: string, task: TaskForm): Promise<RunTaskResponse> {
|
||||||
|
if (task.kind === 'chat.completions') {
|
||||||
|
const result = await createCompatibleChatCompletion(token, {
|
||||||
|
model: task.model,
|
||||||
|
runMode: 'simulation',
|
||||||
|
simulation: true,
|
||||||
|
stream: false,
|
||||||
|
messages: [{ role: 'user', content: task.prompt }],
|
||||||
|
});
|
||||||
|
return { localOnly: true, task: compatibleTask(task, result) };
|
||||||
|
}
|
||||||
|
if (task.kind === 'embeddings') {
|
||||||
|
const result = await createEmbedding(token, {
|
||||||
|
model: task.model,
|
||||||
|
input: embeddingInput(task.prompt),
|
||||||
|
dimensions: task.dimensions,
|
||||||
|
runMode: 'simulation',
|
||||||
|
simulation: true,
|
||||||
|
});
|
||||||
|
return { localOnly: true, task: compatibleTask(task, result) };
|
||||||
|
}
|
||||||
|
if (task.kind === 'reranks') {
|
||||||
|
const result = await createRerank(token, {
|
||||||
|
model: task.model,
|
||||||
|
query: task.prompt,
|
||||||
|
documents: rerankDocuments(task.documents),
|
||||||
|
top_n: task.topN,
|
||||||
|
runMode: 'simulation',
|
||||||
|
simulation: true,
|
||||||
|
});
|
||||||
|
return { localOnly: true, task: compatibleTask(task, result) };
|
||||||
|
}
|
||||||
if (task.kind === 'images.generations') {
|
if (task.kind === 'images.generations') {
|
||||||
return createImageGenerationTask(token, {
|
return createImageGenerationTask(token, {
|
||||||
model: task.model,
|
model: task.model,
|
||||||
@@ -23,10 +60,78 @@ export function runTask(token: string, task: TaskForm): Promise<{ task: GatewayT
|
|||||||
simulation: true,
|
simulation: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return createChatTask(token, {
|
throw new Error(`Unsupported task kind: ${task.kind}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compatibleTask(task: TaskForm, result: Record<string, unknown>): GatewayTask {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
id: `docs-${task.kind}-${Date.now()}`,
|
||||||
|
asyncMode: false,
|
||||||
|
createdAt: now,
|
||||||
|
finishedAt: now,
|
||||||
|
kind: task.kind,
|
||||||
model: task.model,
|
model: task.model,
|
||||||
|
modelType: modelTypeForKind(task.kind),
|
||||||
|
request: requestSnapshot(task),
|
||||||
|
result,
|
||||||
|
runMode: 'simulation',
|
||||||
|
status: 'succeeded',
|
||||||
|
updatedAt: now,
|
||||||
|
userId: 'docs-runner',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSnapshot(task: TaskForm): Record<string, unknown> {
|
||||||
|
if (task.kind === 'embeddings') {
|
||||||
|
return {
|
||||||
|
model: task.model,
|
||||||
|
input: embeddingInput(task.prompt),
|
||||||
|
dimensions: task.dimensions,
|
||||||
|
runMode: 'simulation',
|
||||||
|
simulation: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (task.kind === 'reranks') {
|
||||||
|
return {
|
||||||
|
model: task.model,
|
||||||
|
query: task.prompt,
|
||||||
|
documents: rerankDocuments(task.documents),
|
||||||
|
top_n: task.topN,
|
||||||
|
runMode: 'simulation',
|
||||||
|
simulation: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
model: task.model,
|
||||||
|
messages: [{ role: 'user', content: task.prompt }],
|
||||||
runMode: 'simulation',
|
runMode: 'simulation',
|
||||||
simulation: true,
|
simulation: true,
|
||||||
messages: [{ role: 'user', content: task.prompt }],
|
stream: false,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function embeddingInput(prompt: string) {
|
||||||
|
const lines = splitLines(prompt);
|
||||||
|
return lines.length > 1 ? lines : (lines[0] ?? prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerankDocuments(value?: string) {
|
||||||
|
const documents = splitLines(value ?? '');
|
||||||
|
return documents.length ? documents : ['AI Gateway 提供 OpenAI 兼容接口。', '图片生成任务支持异步队列。'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitLines(value: string) {
|
||||||
|
return value
|
||||||
|
.split(/\n+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelTypeForKind(kind: TaskForm['kind']) {
|
||||||
|
if (kind === 'embeddings') return 'text_embedding';
|
||||||
|
if (kind === 'reranks') return 'text_rerank';
|
||||||
|
if (kind === 'images.generations') return 'image_generate';
|
||||||
|
if (kind === 'images.edits') return 'image_edit';
|
||||||
|
return 'text_generate';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const adminPages = [
|
|||||||
|
|
||||||
export const apiDocPages = [
|
export const apiDocPages = [
|
||||||
{ title: '鉴权与限流', path: '/docs/auth', description: '本地账号、JWT、OpenAPI Key、TPM/RPM/并发限制和错误码。' },
|
{ title: '鉴权与限流', path: '/docs/auth', description: '本地账号、JWT、OpenAPI Key、TPM/RPM/并发限制和错误码。' },
|
||||||
{ title: 'Chat / Responses', path: '/docs/api/chat', description: '对话、stream、结构化输出、取消请求和示例代码。' },
|
{ title: 'Chat / Embeddings / Reranks', path: '/docs/api/chat', description: '对话、文本向量、重排序、鉴权和示例代码。' },
|
||||||
{ title: '图片 / 视频', path: '/docs/api/media', description: '生图、图像编辑、生视频、任务进度和结果取回。' },
|
{ title: '图片 / 视频', path: '/docs/api/media', description: '生图、图像编辑、生视频、任务进度和结果取回。' },
|
||||||
{ title: '在线调用测试', path: '/docs/playground', description: '选择模型和 API Key,编辑参数,查看实时响应和 billings。' },
|
{ title: '在线调用测试', path: '/docs/playground', description: '选择模型和 API Key,编辑参数,查看实时响应和 billings。' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,33 +1,52 @@
|
|||||||
import { useMemo, type FormEvent } from 'react';
|
import { useEffect, useMemo, type FormEvent } from 'react';
|
||||||
import type { GatewayTask } from '@easyai-ai-gateway/contracts';
|
import type { GatewayApiKey, GatewayTask } from '@easyai-ai-gateway/contracts';
|
||||||
import { BookOpen, Play, Search, Send } from 'lucide-react';
|
import { BookOpen, KeyRound, Play, Search, Send } from 'lucide-react';
|
||||||
import { Badge, Button, Select, Textarea } from '../components/ui';
|
import { Badge, Button, Select, Textarea } from '../components/ui';
|
||||||
import type { ApiDocSection, LoadState, TaskForm } from '../types';
|
import type { ApiDocSection, LoadState, TaskForm, TaskKind } from '../types';
|
||||||
|
import { ApiKeySelect, apiKeyNoticeText, resolveSelectedApiKeyId } from './playground-shared';
|
||||||
|
|
||||||
const docs: Array<{ key: ApiDocSection; group: string; method: string; path: string; title: string }> = [
|
interface ApiDocItem {
|
||||||
{ key: 'chat', group: '聊天(Chat)', method: 'POST', path: '/v1/chat/completions', title: 'Chat(聊天)' },
|
group: string;
|
||||||
{ key: 'imageGeneration', group: '图片', method: 'POST', path: '/v1/images/generations', title: '创建图片' },
|
key: ApiDocSection;
|
||||||
{ key: 'imageEdit', group: '图片', method: 'POST', path: '/v1/images/edits', title: '编辑图片' },
|
kind?: TaskKind;
|
||||||
{ key: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估' },
|
lead: string;
|
||||||
{ key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件' },
|
method: string;
|
||||||
|
path: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs: ApiDocItem[] = [
|
||||||
|
{ key: 'chat', group: '文本', kind: 'chat.completions', method: 'POST', path: '/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' },
|
||||||
|
{ 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: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估', lead: '按请求体估算输入输出 token、模型倍率和折扣后的预估费用。' },
|
||||||
|
{ key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源,后续请求可复用返回的文件 URL。' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const guideItems = ['获取 Base URL 和 API Key', '通知设置-WebHook 参数介绍', '错误码', '测试模式'];
|
const guideItems = ['获取 Base URL 和 API Key', '通知设置-WebHook 参数介绍', '错误码', '测试模式'];
|
||||||
|
|
||||||
const taskKindOptions = [
|
const taskKindOptions = [
|
||||||
['chat.completions', 'Chat'],
|
['chat.completions', 'Chat'],
|
||||||
|
['embeddings', '文本向量'],
|
||||||
|
['reranks', '重排序'],
|
||||||
['images.generations', '生图'],
|
['images.generations', '生图'],
|
||||||
['images.edits', '图像编辑'],
|
['images.edits', '图像编辑'],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function ApiDocsPage(props: {
|
export function ApiDocsPage(props: {
|
||||||
activeDocSection: ApiDocSection;
|
activeDocSection: ApiDocSection;
|
||||||
apiKeySecret: string;
|
apiKeySecretsById: Record<string, string>;
|
||||||
|
apiKeys: GatewayApiKey[];
|
||||||
canRun: boolean;
|
canRun: boolean;
|
||||||
coreMessage: string;
|
coreMessage: string;
|
||||||
coreState: LoadState;
|
coreState: LoadState;
|
||||||
|
selectedApiKeyId: string;
|
||||||
taskForm: TaskForm;
|
taskForm: TaskForm;
|
||||||
taskResult: GatewayTask | null;
|
taskResult: GatewayTask | null;
|
||||||
|
onApiKeyChange: (apiKeyId: string) => void;
|
||||||
|
onCreateApiKey: () => void;
|
||||||
onLogin: () => void;
|
onLogin: () => void;
|
||||||
onDocSectionChange: (value: ApiDocSection) => void;
|
onDocSectionChange: (value: ApiDocSection) => void;
|
||||||
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
|
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
|
||||||
@@ -35,8 +54,16 @@ export function ApiDocsPage(props: {
|
|||||||
}) {
|
}) {
|
||||||
const current = docs.find((item) => item.key === props.activeDocSection) ?? docs[0];
|
const current = docs.find((item) => item.key === props.activeDocSection) ?? docs[0];
|
||||||
const isFileDoc = current.key === 'files';
|
const isFileDoc = current.key === 'files';
|
||||||
|
const apiKeyNotice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById);
|
||||||
|
const activeApiKeyId = resolveSelectedApiKeyId(props.apiKeys, props.apiKeySecretsById, props.selectedApiKeyId);
|
||||||
const bodyExample = useMemo(() => requestBodyExample(props.taskForm), [props.taskForm]);
|
const bodyExample = useMemo(() => requestBodyExample(props.taskForm), [props.taskForm]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (current.kind && props.taskForm.kind !== current.kind) {
|
||||||
|
props.onTaskFormChange(defaultTaskForKind(current.kind, props.taskForm));
|
||||||
|
}
|
||||||
|
}, [current.kind, props.taskForm.kind]);
|
||||||
|
|
||||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
if (!props.canRun) {
|
if (!props.canRun) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -46,6 +73,21 @@ export function ApiDocsPage(props: {
|
|||||||
props.onSubmitTask(event);
|
props.onSubmitTask(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDocClick(item: ApiDocItem) {
|
||||||
|
if (item.kind) {
|
||||||
|
props.onTaskFormChange(defaultTaskForKind(item.kind, props.taskForm));
|
||||||
|
}
|
||||||
|
props.onDocSectionChange(item.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKindChange(kind: TaskKind) {
|
||||||
|
props.onTaskFormChange(defaultTaskForKind(kind, props.taskForm));
|
||||||
|
const nextSection = docSectionForKind(kind);
|
||||||
|
if (nextSection !== props.activeDocSection) {
|
||||||
|
props.onDocSectionChange(nextSection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="apiDocsShell">
|
<div className="apiDocsShell">
|
||||||
<aside className="docsSidebar">
|
<aside className="docsSidebar">
|
||||||
@@ -66,7 +108,7 @@ export function ApiDocsPage(props: {
|
|||||||
active: item.key === props.activeDocSection,
|
active: item.key === props.activeDocSection,
|
||||||
method: item.method,
|
method: item.method,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
onClick: () => props.onDocSectionChange(item.key),
|
onClick: () => handleDocClick(item),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -79,9 +121,7 @@ export function ApiDocsPage(props: {
|
|||||||
<Badge variant="warning">{current.method}</Badge>
|
<Badge variant="warning">{current.method}</Badge>
|
||||||
<code>{current.path}</code>
|
<code>{current.path}</code>
|
||||||
</div>
|
</div>
|
||||||
<p className="docsLead">
|
<p className="docsLead">{current.lead}</p>
|
||||||
保持与原 integration-platform / OpenAI 兼容接口一致,支持本地 API Key 和 server-main 接入 token。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<section className="paramCard">
|
<section className="paramCard">
|
||||||
<header>
|
<header>
|
||||||
@@ -90,7 +130,7 @@ export function ApiDocsPage(props: {
|
|||||||
</header>
|
</header>
|
||||||
<ParamRow name="Content-Type" type="string" required value={isFileDoc ? 'multipart/form-data' : 'application/json'} />
|
<ParamRow name="Content-Type" type="string" required value={isFileDoc ? 'multipart/form-data' : 'application/json'} />
|
||||||
<ParamRow name="Accept" type="string" required value="application/json" />
|
<ParamRow name="Accept" type="string" required value="application/json" />
|
||||||
<ParamRow name="Authorization" type="string" value="Bearer {{YOUR_API_KEY}}" />
|
<ParamRow name="Authorization" type="string" required value="Bearer {{YOUR_API_KEY}},支持本地 API Key;管理接口仍使用 JWT。向量需 embedding 权限,重排序需 rerank 权限。" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="paramCard">
|
<section className="paramCard">
|
||||||
@@ -104,12 +144,9 @@ export function ApiDocsPage(props: {
|
|||||||
<ParamRow name="source" type="string" value="上传来源标记" />
|
<ParamRow name="source" type="string" value="上传来源标记" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
bodyParamRows(current.key).map((row) => (
|
||||||
<ParamRow name="model" type="string" required value="模型 ID 或别名" />
|
<ParamRow key={row.name} {...row} />
|
||||||
<ParamRow name="messages / prompt" type="array|string" required value="对话消息或图片提示词" />
|
))
|
||||||
<ParamRow name="simulation" type="boolean" value="测试模式开关" />
|
|
||||||
<ParamRow name="stream" type="boolean" value="对话进度流式返回" />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -127,9 +164,27 @@ export function ApiDocsPage(props: {
|
|||||||
<Badge variant="warning">POST</Badge>
|
<Badge variant="warning">POST</Badge>
|
||||||
<span>{current.path}</span>
|
<span>{current.path}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<label className="shLabel">
|
||||||
|
API Key
|
||||||
|
<ApiKeySelect
|
||||||
|
apiKeySecretsById={props.apiKeySecretsById}
|
||||||
|
apiKeys={props.apiKeys}
|
||||||
|
selectedApiKeyId={activeApiKeyId}
|
||||||
|
onApiKeyChange={props.onApiKeyChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{apiKeyNotice && (
|
||||||
|
<div className="docsKeyNotice">
|
||||||
|
<span>{apiKeyNotice}</span>
|
||||||
|
<Button type="button" size="sm" variant="secondary" onClick={props.onCreateApiKey}>
|
||||||
|
<KeyRound size={14} />
|
||||||
|
创建 Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<label className="shLabel">
|
<label className="shLabel">
|
||||||
能力
|
能力
|
||||||
<Select value={props.taskForm.kind} onChange={(event) => props.onTaskFormChange(defaultTaskForKind(event.target.value as TaskForm['kind'], props.taskForm))}>
|
<Select value={props.taskForm.kind} onChange={(event) => handleKindChange(event.target.value as TaskKind)}>
|
||||||
{taskKindOptions.map(([value, label]) => (
|
{taskKindOptions.map(([value, label]) => (
|
||||||
<option value={value} key={value}>{label}</option>
|
<option value={value} key={value}>{label}</option>
|
||||||
))}
|
))}
|
||||||
@@ -198,16 +253,39 @@ function groupDocs(items: typeof docs) {
|
|||||||
|
|
||||||
function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm {
|
function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm {
|
||||||
if (kind === 'chat.completions') return { ...current, kind, model: 'gpt-4o-mini' };
|
if (kind === 'chat.completions') return { ...current, kind, model: 'gpt-4o-mini' };
|
||||||
|
if (kind === 'embeddings') {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
dimensions: current.dimensions ?? 4,
|
||||||
|
kind,
|
||||||
|
model: 'text-embedding-v4',
|
||||||
|
prompt: current.prompt || 'AI Gateway 提供 OpenAI 兼容接口。',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (kind === 'reranks') {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
documents: current.documents ?? 'AI Gateway 提供 OpenAI 兼容接口。\n图片生成任务支持异步队列。\n文本向量接口返回 embedding 数组。',
|
||||||
|
kind,
|
||||||
|
model: 'qwen3-rerank',
|
||||||
|
prompt: current.prompt || 'OpenAI 兼容接口',
|
||||||
|
topN: current.topN ?? 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (kind === 'images.edits') return { ...current, kind, image: current.image ?? 'https://example.com/source.png', mask: current.mask ?? 'https://example.com/mask.png', model: 'gpt-image-1' };
|
if (kind === 'images.edits') return { ...current, kind, image: current.image ?? 'https://example.com/source.png', mask: current.mask ?? 'https://example.com/mask.png', model: 'gpt-image-1' };
|
||||||
return { ...current, kind, model: 'gpt-image-1' };
|
return { ...current, kind, model: 'gpt-image-1' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestBodyExample(task: TaskForm) {
|
function requestBodyExample(task: TaskForm) {
|
||||||
const body = task.kind === 'chat.completions'
|
const body = task.kind === 'chat.completions'
|
||||||
? { model: task.model, messages: [{ role: 'user', content: task.prompt }], simulation: true, stream: true }
|
? { model: task.model, messages: [{ role: 'user', content: task.prompt }], runMode: 'simulation', simulation: true, stream: false }
|
||||||
: task.kind === 'images.edits'
|
: task.kind === 'embeddings'
|
||||||
? { model: task.model, prompt: task.prompt, image: task.image, mask: task.mask, simulation: true }
|
? { model: task.model, input: embeddingInputExample(task.prompt), dimensions: task.dimensions ?? 4, runMode: 'simulation', simulation: true }
|
||||||
: { model: task.model, prompt: task.prompt, quality: 'medium', simulation: true, size: '1024x1024' };
|
: task.kind === 'reranks'
|
||||||
|
? { model: task.model, query: task.prompt, documents: rerankDocumentsExample(task.documents), top_n: task.topN ?? 2, runMode: 'simulation', simulation: true }
|
||||||
|
: task.kind === 'images.edits'
|
||||||
|
? { model: task.model, prompt: task.prompt, image: task.image, mask: task.mask, runMode: 'simulation', simulation: true }
|
||||||
|
: { model: task.model, prompt: task.prompt, quality: 'medium', runMode: 'simulation', simulation: true, size: '1024x1024' };
|
||||||
return JSON.stringify(body, null, 2);
|
return JSON.stringify(body, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,15 +297,84 @@ function parseBody(value: string, current: TaskForm): TaskForm {
|
|||||||
messages?: Array<{ content?: string }>;
|
messages?: Array<{ content?: string }>;
|
||||||
model?: string;
|
model?: string;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
|
input?: string | string[];
|
||||||
|
query?: string;
|
||||||
|
documents?: string[];
|
||||||
|
top_n?: number;
|
||||||
|
dimensions?: number;
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
|
dimensions: numberOrCurrent(body.dimensions, current.dimensions),
|
||||||
|
documents: Array.isArray(body.documents) ? body.documents.join('\n') : current.documents,
|
||||||
image: body.image ?? current.image,
|
image: body.image ?? current.image,
|
||||||
mask: body.mask ?? current.mask,
|
mask: body.mask ?? current.mask,
|
||||||
model: body.model ?? current.model,
|
model: body.model ?? current.model,
|
||||||
prompt: body.prompt ?? body.messages?.[0]?.content ?? current.prompt,
|
prompt: body.prompt ?? body.query ?? inputText(body.input) ?? body.messages?.[0]?.content ?? current.prompt,
|
||||||
|
topN: numberOrCurrent(body.top_n, current.topN),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bodyParamRows(section: ApiDocSection) {
|
||||||
|
if (section === 'embeddings') {
|
||||||
|
return [
|
||||||
|
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名,例如 text-embedding-v4' },
|
||||||
|
{ name: 'input', type: 'string|array', required: true, value: '需要向量化的文本或文本数组' },
|
||||||
|
{ name: 'dimensions', type: 'number', value: '可选向量维度,需模型支持' },
|
||||||
|
{ name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation,不消耗真实上游额度' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (section === 'reranks') {
|
||||||
|
return [
|
||||||
|
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名,例如 qwen3-rerank' },
|
||||||
|
{ name: 'query', type: 'string', required: true, value: '用于相关性排序的查询文本' },
|
||||||
|
{ name: 'documents', type: 'array', required: true, value: '候选文档文本数组,至少 1 条' },
|
||||||
|
{ name: 'top_n', type: 'number', value: '返回前 N 条结果' },
|
||||||
|
{ name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation,不消耗真实上游额度' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ name: 'model', type: 'string', required: true, value: '模型 ID 或别名' },
|
||||||
|
{ name: 'messages / prompt', type: 'array|string', required: true, value: '对话消息或图片提示词' },
|
||||||
|
{ name: 'simulation', type: 'boolean', value: '测试模式开关' },
|
||||||
|
{ name: 'stream', type: 'boolean', value: '对话进度流式返回' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function docSectionForKind(kind: TaskKind): ApiDocSection {
|
||||||
|
if (kind === 'embeddings') return 'embeddings';
|
||||||
|
if (kind === 'reranks') return 'reranks';
|
||||||
|
if (kind === 'images.generations') return 'imageGeneration';
|
||||||
|
if (kind === 'images.edits') return 'imageEdit';
|
||||||
|
return 'chat';
|
||||||
|
}
|
||||||
|
|
||||||
|
function embeddingInputExample(prompt: string) {
|
||||||
|
const lines = splitLines(prompt);
|
||||||
|
return lines.length > 1 ? lines : (lines[0] ?? prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerankDocumentsExample(value?: string) {
|
||||||
|
return splitLines(value ?? '').length
|
||||||
|
? splitLines(value ?? '')
|
||||||
|
: ['AI Gateway 提供 OpenAI 兼容接口。', '图片生成任务支持异步队列。'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitLines(value: string) {
|
||||||
|
return value
|
||||||
|
.split(/\n+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputText(value: string | string[] | undefined) {
|
||||||
|
if (Array.isArray(value)) return value.join('\n');
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberOrCurrent(value: unknown, current?: number) {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? value : current;
|
||||||
|
}
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ function modelTypeMatchesCapability(value: string, capability: string) {
|
|||||||
function canonicalCapabilityValue(value: string) {
|
function canonicalCapabilityValue(value: string) {
|
||||||
const type = value.trim().toLowerCase().replaceAll('-', '_');
|
const type = value.trim().toLowerCase().replaceAll('-', '_');
|
||||||
if (type === 'embedding') return 'text_embedding';
|
if (type === 'embedding') return 'text_embedding';
|
||||||
|
if (type === 'rerank' || type === 'reranks') return 'text_rerank';
|
||||||
if (type === 'model') return 'model_3d';
|
if (type === 'model') return 'model_3d';
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
@@ -265,6 +266,7 @@ function capabilityValueForTag(tag: string) {
|
|||||||
推理: 'reasoning',
|
推理: 'reasoning',
|
||||||
结构化输出: 'structured_output',
|
结构化输出: 'structured_output',
|
||||||
数字人: 'digital_human',
|
数字人: 'digital_human',
|
||||||
|
重排序: 'text_rerank',
|
||||||
'3D 模型': 'model_3d',
|
'3D 模型': 'model_3d',
|
||||||
'文生 3D': 'text_to_model',
|
'文生 3D': 'text_to_model',
|
||||||
'图生 3D': 'image_to_model',
|
'图生 3D': 'image_to_model',
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type PlatformModelTypeDefinition = {
|
|||||||
export const platformModelTypeDefinitions: PlatformModelTypeDefinition[] = [
|
export const platformModelTypeDefinitions: PlatformModelTypeDefinition[] = [
|
||||||
{ key: 'text_generate', label: '文本生成', group: '文本', area: 'text' },
|
{ key: 'text_generate', label: '文本生成', group: '文本', area: 'text' },
|
||||||
{ key: 'text_embedding', label: '文本向量', group: '文本', area: 'embedding' },
|
{ key: 'text_embedding', label: '文本向量', group: '文本', area: 'embedding' },
|
||||||
|
{ key: 'text_rerank', label: '重排序', group: '文本', area: 'text' },
|
||||||
{ key: 'image_generate', label: '图像生成', group: '图像', area: 'image' },
|
{ key: 'image_generate', label: '图像生成', group: '图像', area: 'image' },
|
||||||
{ key: 'image_edit', label: '图像编辑', group: '图像', area: 'image' },
|
{ key: 'image_edit', label: '图像编辑', group: '图像', area: 'image' },
|
||||||
{ key: 'image_analysis', label: '图像理解', group: '理解', area: 'text' },
|
{ key: 'image_analysis', label: '图像理解', group: '理解', area: 'text' },
|
||||||
@@ -469,7 +470,7 @@ function boolFrom(value: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function looksLikeCapabilityType(key: string) {
|
function looksLikeCapabilityType(key: string) {
|
||||||
return modelTypeDefinitionMap.has(key) || key.includes('_') || ['chat', 'image', 'video', 'audio', 'embedding', 'music', 'digital_human', 'model_3d'].includes(key);
|
return modelTypeDefinitionMap.has(key) || key.includes('_') || ['chat', 'image', 'video', 'audio', 'embedding', 'rerank', 'music', 'digital_human', 'model_3d'].includes(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferArea(type: string): CapabilityConfigArea {
|
function inferArea(type: string): CapabilityConfigArea {
|
||||||
@@ -484,7 +485,7 @@ function inferArea(type: string): CapabilityConfigArea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isTextLike(type: string) {
|
function isTextLike(type: string) {
|
||||||
return ['text_generate', 'image_analysis', 'video_understanding', 'audio_understanding', 'omni', 'tools_call'].includes(type);
|
return ['text_generate', 'text_rerank', 'image_analysis', 'video_understanding', 'audio_understanding', 'omni', 'tools_call'].includes(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImageLike(type: string) {
|
function isImageLike(type: string) {
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ const adminPaths: Record<AdminSection, string> = {
|
|||||||
|
|
||||||
const docsPaths: Record<ApiDocSection, string> = {
|
const docsPaths: Record<ApiDocSection, string> = {
|
||||||
chat: '/docs/chat',
|
chat: '/docs/chat',
|
||||||
|
embeddings: '/docs/embeddings',
|
||||||
|
reranks: '/docs/reranks',
|
||||||
imageGeneration: '/docs/images/generations',
|
imageGeneration: '/docs/images/generations',
|
||||||
imageEdit: '/docs/images/edits',
|
imageEdit: '/docs/images/edits',
|
||||||
pricing: '/docs/pricing',
|
pricing: '/docs/pricing',
|
||||||
@@ -134,6 +136,8 @@ function parseAdminSection(path: string): AdminSection {
|
|||||||
function parseDocSection(path: string): ApiDocSection {
|
function parseDocSection(path: string): ApiDocSection {
|
||||||
if (path === '/docs') return 'chat';
|
if (path === '/docs') return 'chat';
|
||||||
if (path === '/docs/api/chat') return 'chat';
|
if (path === '/docs/api/chat') return 'chat';
|
||||||
|
if (path === '/docs/api/embeddings') return 'embeddings';
|
||||||
|
if (path === '/docs/api/reranks') return 'reranks';
|
||||||
if (path === '/docs/api/media') return 'imageGeneration';
|
if (path === '/docs/api/media') return 'imageGeneration';
|
||||||
if (path === '/docs/playground') return 'chat';
|
if (path === '/docs/playground') return 'chat';
|
||||||
return docsSections[path] ?? 'chat';
|
return docsSections[path] ?? 'chat';
|
||||||
|
|||||||
@@ -98,6 +98,22 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docsKeyNotice {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #92400e;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docsKeyNotice .shButton {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
.docsLead {
|
.docsLead {
|
||||||
margin: 18px 0 24px;
|
margin: 18px 0 24px;
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export type LoadState = 'idle' | 'loading' | 'ready' | 'error';
|
export type LoadState = 'idle' | 'loading' | 'ready' | 'error';
|
||||||
export type AuthMode = 'login' | 'register' | 'external';
|
export type AuthMode = 'login' | 'register' | 'external';
|
||||||
export type TaskKind = 'chat.completions' | 'images.generations' | 'images.edits';
|
export type TaskKind = 'chat.completions' | 'embeddings' | 'reranks' | 'images.generations' | 'images.edits';
|
||||||
export type PageKey = 'home' | 'playground' | 'models' | 'workspace' | 'admin' | 'docs';
|
export type PageKey = 'home' | 'playground' | 'models' | 'workspace' | 'admin' | 'docs';
|
||||||
export type PlaygroundMode = 'chat' | 'image' | 'video';
|
export type PlaygroundMode = 'chat' | 'image' | 'video';
|
||||||
export type WorkspaceSection = 'overview' | 'billing' | 'apiKeys' | 'tasks' | 'transactions';
|
export type WorkspaceSection = 'overview' | 'billing' | 'apiKeys' | 'tasks' | 'transactions';
|
||||||
export type ApiDocSection = 'chat' | 'imageGeneration' | 'imageEdit' | 'pricing' | 'files';
|
export type ApiDocSection = 'chat' | 'embeddings' | 'reranks' | 'imageGeneration' | 'imageEdit' | 'pricing' | 'files';
|
||||||
export type AdminSection =
|
export type AdminSection =
|
||||||
| 'overview'
|
| 'overview'
|
||||||
| 'globalModels'
|
| 'globalModels'
|
||||||
@@ -37,8 +37,11 @@ export interface TaskForm {
|
|||||||
kind: TaskKind;
|
kind: TaskKind;
|
||||||
model: string;
|
model: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
dimensions?: number;
|
||||||
|
documents?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
mask?: string;
|
mask?: string;
|
||||||
|
topN?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiKeyForm {
|
export interface ApiKeyForm {
|
||||||
|
|||||||
Reference in New Issue
Block a user