chore(merge): 整合最新 main 与统一认证变更
ci / verify (pull_request) Successful in 12m18s

合并远端生产 CI、依赖与 API 文档改动,保留统一认证和 SSF 能力。由于远端生产基线已占用 0062,将尚未发布的身份迁移整理为 0063 至 0068 的最终增量 Schema,移除仅用于开发期草稿升级的破坏性迁移步骤。

验证:go test ./...。其余生产门禁在合并提交后继续执行。
This commit is contained in:
2026-07-17 19:06:37 +08:00
100 changed files with 9673 additions and 1799 deletions
@@ -0,0 +1,98 @@
package httpapi
import (
"net/http"
gatewaydocs "github.com/easyai/easyai-ai-gateway/apps/api/docs"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/skillbundle"
)
const (
opsManagementSkillDownloadPath = "/api/v1/public/skills/ai-gateway-ops-management/download"
apiDocsJSONPath = "/api-docs-json"
apiDocsYAMLPath = "/api-docs-yaml"
)
// getOpsManagementSkillMetadata godoc
// @Summary 获取 AI Gateway 运维管理 SKILL 元数据
// @Description 返回公开运维管理 SKILL 的名称、版本、模块、下载文件名和机器可读接口文档路径。
// @Tags agent-resources
// @Produce json
// @Success 200 {object} SkillBundleMetadataResponse
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/public/skills/ai-gateway-ops-management/metadata [get]
func (s *Server) getOpsManagementSkillMetadata(w http.ResponseWriter, _ *http.Request) {
metadata, err := skillbundle.LoadMetadata()
if err != nil {
s.logger.Error("load operations skill metadata failed", "error", err)
writeError(w, http.StatusInternalServerError, "operations skill metadata unavailable")
return
}
writeJSON(w, http.StatusOK, opsManagementSkillMetadataResponse(metadata))
}
// downloadOpsManagementSkill godoc
// @Summary 下载 AI Gateway 运维管理 SKILL
// @Description 下载可交给 Agent 使用的 ai-gateway-ops-management ZIP 包。
// @Tags agent-resources
// @Produce application/zip
// @Success 200 {file} binary
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/public/skills/ai-gateway-ops-management/download [get]
func (s *Server) downloadOpsManagementSkill(w http.ResponseWriter, _ *http.Request) {
metadata, err := skillbundle.LoadMetadata()
if err != nil {
s.logger.Error("load operations skill metadata failed", "error", err)
writeError(w, http.StatusInternalServerError, "operations skill metadata unavailable")
return
}
archive, err := skillbundle.BuildArchive()
if err != nil {
s.logger.Error("build operations skill archive failed", "error", err)
writeError(w, http.StatusInternalServerError, "operations skill download unavailable")
return
}
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", `attachment; filename="`+skillbundle.FileName(metadata)+`"`)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(archive)
}
// apiDocsJSON godoc
// @Summary 获取 AI Gateway Swagger JSON
// @Description 返回当前构建内嵌的完整机器可读 Swagger JSON,供 Agent 在 SKILL references 未覆盖接口时查询。
// @Tags agent-resources
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api-docs-json [get]
func (s *Server) apiDocsJSON(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(gatewaydocs.SwaggerJSON)
}
// apiDocsYAML godoc
// @Summary 获取 AI Gateway Swagger YAML
// @Description 返回当前构建内嵌的完整机器可读 Swagger YAML。
// @Tags agent-resources
// @Produce application/yaml
// @Success 200 {string} string
// @Router /api-docs-yaml [get]
func (s *Server) apiDocsYAML(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(gatewaydocs.SwaggerYAML)
}
func opsManagementSkillMetadataResponse(metadata skillbundle.Metadata) SkillBundleMetadataResponse {
return SkillBundleMetadataResponse{
Name: metadata.Name,
Version: metadata.Version,
DisplayName: skillbundle.DisplayName,
Modules: metadata.Modules,
FileName: skillbundle.FileName(metadata),
DownloadPath: opsManagementSkillDownloadPath,
APIDocsJSONPath: apiDocsJSONPath,
APIDocsYAMLPath: apiDocsYAMLPath,
}
}
@@ -0,0 +1,115 @@
package httpapi
import (
"archive/zip"
"bytes"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestGetOpsManagementSkillMetadata(t *testing.T) {
server := &Server{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
request := httptest.NewRequest(http.MethodGet, "/api/v1/public/skills/ai-gateway-ops-management/metadata", nil)
response := httptest.NewRecorder()
server.getOpsManagementSkillMetadata(response, request)
if response.Code != http.StatusOK {
t.Fatalf("expected metadata status 200, got %d: %s", response.Code, response.Body.String())
}
var metadata SkillBundleMetadataResponse
if err := json.Unmarshal(response.Body.Bytes(), &metadata); err != nil {
t.Fatalf("decode metadata: %v", err)
}
if metadata.Name != "ai-gateway-ops-management" || metadata.Version != "1.0.2" {
t.Fatalf("unexpected metadata: %+v", metadata)
}
if len(metadata.Modules) != 1 || metadata.Modules[0] != "model-runtime" {
t.Fatalf("unexpected metadata modules: %+v", metadata.Modules)
}
if metadata.APIDocsJSONPath != "/api-docs-json" || metadata.APIDocsYAMLPath != "/api-docs-yaml" {
t.Fatalf("unexpected API docs paths: %+v", metadata)
}
}
func TestDownloadOpsManagementSkill(t *testing.T) {
server := &Server{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
request := httptest.NewRequest(http.MethodGet, "/api/v1/public/skills/ai-gateway-ops-management/download", nil)
response := httptest.NewRecorder()
server.downloadOpsManagementSkill(response, request)
if response.Code != http.StatusOK {
t.Fatalf("expected download status 200, got %d: %s", response.Code, response.Body.String())
}
if response.Header().Get("Content-Type") != "application/zip" {
t.Fatalf("unexpected content type: %q", response.Header().Get("Content-Type"))
}
if disposition := response.Header().Get("Content-Disposition"); !strings.Contains(disposition, "ai-gateway-ops-management-v1.0.2.zip") {
t.Fatalf("unexpected content disposition: %q", disposition)
}
raw := response.Body.Bytes()
archive, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
if err != nil {
t.Fatalf("open downloaded archive: %v", err)
}
foundSkill := false
for _, file := range archive.File {
if file.Name == "SKILL.md" {
foundSkill = true
break
}
}
if !foundSkill {
t.Fatalf("downloaded archive does not contain SKILL.md")
}
}
func TestEmbeddedAPIDocs(t *testing.T) {
server := &Server{}
jsonResponse := httptest.NewRecorder()
server.apiDocsJSON(jsonResponse, httptest.NewRequest(http.MethodGet, "/api-docs-json", nil))
if jsonResponse.Code != http.StatusOK {
t.Fatalf("expected JSON docs status 200, got %d", jsonResponse.Code)
}
if jsonResponse.Header().Get("Content-Type") != "application/json; charset=utf-8" {
t.Fatalf("unexpected JSON docs content type: %q", jsonResponse.Header().Get("Content-Type"))
}
var document struct {
Paths map[string]json.RawMessage `json:"paths"`
}
if err := json.Unmarshal(jsonResponse.Body.Bytes(), &document); err != nil {
t.Fatalf("decode embedded Swagger JSON: %v", err)
}
for _, path := range []string{
"/api-docs-json",
"/api/v1/public/skills/ai-gateway-ops-management/download",
"/api/admin/catalog/providers",
"/api/admin/catalog/base-models",
"/api/admin/platforms",
"/api/admin/runtime/policy-sets",
"/api/admin/pricing/rule-sets",
} {
if document.Paths[path] == nil {
t.Fatalf("embedded Swagger JSON missing %q", path)
}
}
yamlResponse := httptest.NewRecorder()
server.apiDocsYAML(yamlResponse, httptest.NewRequest(http.MethodGet, "/api-docs-yaml", nil))
if yamlResponse.Code != http.StatusOK {
t.Fatalf("expected YAML docs status 200, got %d", yamlResponse.Code)
}
if yamlResponse.Header().Get("Content-Type") != "application/yaml; charset=utf-8" {
t.Fatalf("unexpected YAML docs content type: %q", yamlResponse.Header().Get("Content-Type"))
}
if !strings.Contains(yamlResponse.Body.String(), "/api/v1/public/skills/ai-gateway-ops-management/metadata") {
t.Fatalf("embedded Swagger YAML missing operations skill metadata path")
}
}
@@ -142,7 +142,7 @@ func TestWriteCompatibleTaskResponseMapsInvalidParameterToBadRequest(t *testing.
executor := &fakeTaskExecutor{
runErr: &clients.ClientError{
Code: "invalid_parameter",
Message: "reasoning_effort must be one of: none, minimal, low, medium, high, xhigh",
Message: "reasoning_effort must be one of: none, minimal, low, medium, high, xhigh, max",
Retryable: false,
},
}
@@ -157,6 +157,9 @@ func TestWriteCompatibleTaskResponseMapsInvalidParameterToBadRequest(t *testing.
if !strings.Contains(recorder.Body.String(), "invalid_parameter") {
t.Fatalf("response should include invalid_parameter code: %s", recorder.Body.String())
}
if !strings.Contains(recorder.Body.String(), `"type":"invalid_request_error"`) || !strings.Contains(recorder.Body.String(), `"param":null`) {
t.Fatalf("response should use OpenAI error fields: %s", recorder.Body.String())
}
}
func TestWriteCompatibleTaskResponseReturnsSSEWhenStreamIsTrue(t *testing.T) {
+40 -4
View File
@@ -429,6 +429,10 @@ func (s *Server) createPlatformModel(w http.ResponseWriter, r *http.Request) {
}
model, err := s.store.CreatePlatformModel(r.Context(), input)
if err != nil {
if errors.Is(err, store.ErrInvalidPlatformModelConfiguration) {
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
return
}
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "base model not found")
return
@@ -473,6 +477,10 @@ func (s *Server) replacePlatformModels(w http.ResponseWriter, r *http.Request) {
models, err := s.store.ReplacePlatformModels(r.Context(), platformID, input.Models)
if err != nil {
if errors.Is(err, store.ErrInvalidPlatformModelConfiguration) {
writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter")
return
}
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "base model not found")
return
@@ -979,7 +987,6 @@ func (s *Server) listModelRateLimitStatuses(w http.ResponseWriter, r *http.Reque
// @Failure 404 {object} ErrorEnvelope
// @Failure 429 {object} ErrorEnvelope
// @Failure 502 {object} ErrorEnvelope
// @Router /api/v1/responses [post]
// @Router /api/v1/embeddings [post]
// @Router /api/v1/reranks [post]
// @Router /api/v1/images/generations [post]
@@ -989,8 +996,6 @@ func (s *Server) listModelRateLimitStatuses(w http.ResponseWriter, r *http.Reque
// @Router /api/v1/music/generations [post]
// @Router /api/v1/speech/generations [post]
// @Router /api/v1/voice_clone [post]
// @Router /chat/completions [post]
// @Router /v1/chat/completions [post]
// @Router /embeddings [post]
// @Router /v1/embeddings [post]
// @Router /reranks [post]
@@ -1024,6 +1029,12 @@ func (s *Server) createTask(kind string, compatible bool) http.Handler {
writeError(w, status, err.Error(), clients.ErrorCode(err))
return
}
if kind == "chat.completions" || kind == "responses" {
if err := clients.ValidateOpenAIRequestParameters(kind, body); err != nil {
writeErrorWithDetails(w, http.StatusBadRequest, err.Error(), map[string]any{"param": clients.ErrorParam(err)}, clients.ErrorCode(err))
return
}
}
model := requestModelName(body)
if model == "" {
writeError(w, http.StatusBadRequest, "model is required")
@@ -1095,7 +1106,7 @@ func (s *Server) createTask(kind string, compatible bool) http.Handler {
// @Produce text/event-stream
// @Security BearerAuth
// @Param X-Async header bool false "该接口忽略此参数"
// @Param input body TaskRequest true "Chat Completions 请求"
// @Param input body ChatCompletionRequest true "Chat Completions 请求"
// @Success 200 {object} ChatCompletionCompatibleResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
@@ -1109,6 +1120,26 @@ func (s *Server) createAPIV1ChatCompletions() http.Handler {
return s.createTask("chat.completions", false)
}
// openAIChatCompletionsDoc godoc
// @Summary 创建 OpenAI Chat Completions
// @Description OpenAI-compatible Chat Completions 入口;仅接受官方字段及文档声明的 EasyAI 路由扩展,未知顶层字段返回 400 invalid_parameter。
// @Tags chat
// @Accept json
// @Produce json
// @Produce text/event-stream
// @Security BearerAuth
// @Param input body ChatCompletionRequest true "Chat Completions 请求"
// @Success 200 {object} ChatCompletionCompatibleResponse
// @Failure 400 {object} ErrorEnvelope "invalid_parameter"
// @Failure 401 {object} ErrorEnvelope
// @Failure 402 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 429 {object} ErrorEnvelope
// @Failure 502 {object} ErrorEnvelope
// @Router /chat/completions [post]
// @Router /v1/chat/completions [post]
func openAIChatCompletionsDoc() {}
// openAIResponsesDoc godoc
// @Summary 创建 OpenAI Responses
// @Description 公开 OpenAI-compatible Responses 入口。模型声明 openai_responses 时原生转发,否则使用 Chat Completions 转换;store 缺省为 true。previous_response_id 严格绑定首次成功的平台模型和上游协议,链路不可用时不跨平台续接。未提供 previous_response_id 时由调用方管理完整状态,Gateway 以本轮 input/messages 为准且不追加本地历史。
@@ -1127,6 +1158,7 @@ func (s *Server) createAPIV1ChatCompletions() http.Handler {
// @Failure 503 {object} ErrorEnvelope "response_chain_unavailable"
// @Router /responses [post]
// @Router /v1/responses [post]
// @Router /api/v1/responses [post]
func openAIResponsesDoc() {}
func (s *Server) requestExecutionContext(r *http.Request) (context.Context, context.CancelFunc) {
@@ -1357,6 +1389,10 @@ func statusFromRunError(err error) int {
return http.StatusServiceUnavailable
case clients.ErrorCode(err) == "bad_request" || clients.ErrorCode(err) == "invalid_parameter" || clients.ErrorCode(err) == "cloned_voice_expired" || clients.ErrorCode(err) == "cloned_voice_unavailable" || clients.ErrorCode(err) == "cloned_voice_platform_unavailable" || clients.ErrorCode(err) == "unsupported_operation" || clients.ErrorCode(err) == "invalid_proxy":
return http.StatusBadRequest
case store.ModelCandidateErrorCode(err) == "invalid_parameter":
return http.StatusBadRequest
case store.ModelCandidateErrorCode(err) == "model_capability_configuration_error":
return http.StatusInternalServerError
case clients.ErrorCode(err) == "cloned_voice_not_found":
return http.StatusNotFound
case store.ModelCandidateErrorCode(err) == "platform_cooling_down" || store.ModelCandidateErrorCode(err) == "model_cooling_down":
@@ -0,0 +1,194 @@
package httpapi
import (
"context"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestKeling30TurboSimulationFlow(t *testing.T) {
databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run the Kling simulation integration flow")
}
ctx := context.Background()
applyMigration(t, ctx, databaseURL)
db, err := store.Connect(ctx, databaseURL)
if err != nil {
t.Fatalf("connect store: %v", err)
}
defer db.Close()
serverCtx, cancelServer := context.WithCancel(ctx)
defer cancelServer()
server := httptest.NewServer(NewServerWithContext(serverCtx, config.Config{
AppEnv: "test",
HTTPAddr: ":0",
DatabaseURL: databaseURL,
IdentityMode: "hybrid",
JWTSecret: "test-secret",
CORSAllowedOrigin: "*",
}, db, slog.New(slog.NewTextHandler(io.Discard, nil))))
defer server.Close()
suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
username := "kling_sim_" + suffix
password := "password123"
var registerResponse struct {
AccessToken string `json:"accessToken"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/auth/register", "", map[string]any{
"username": username,
"email": username + "@example.com",
"password": password,
}, http.StatusCreated, &registerResponse)
var apiKeyResponse struct {
Secret string `json:"secret"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/api-keys", registerResponse.AccessToken, map[string]any{
"name": "kling simulation key",
}, http.StatusCreated, &apiKeyResponse)
if _, err := db.Pool().Exec(ctx, `
UPDATE gateway_users
SET roles = '["admin"]'::jsonb
WHERE username = $1`, username); err != nil {
t.Fatalf("promote Kling simulation user: %v", err)
}
var loginResponse struct {
AccessToken string `json:"accessToken"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/auth/login", "", map[string]any{
"account": username,
"password": password,
}, http.StatusOK, &loginResponse)
var gatewayUserID string
if err := db.Pool().QueryRow(
ctx,
`SELECT id::text FROM gateway_users WHERE username = $1`,
username,
).Scan(&gatewayUserID); err != nil {
t.Fatalf("read Kling simulation user id: %v", err)
}
doJSON(t, server.URL, http.MethodPatch, "/api/admin/users/"+gatewayUserID+"/wallet", loginResponse.AccessToken, map[string]any{
"currency": "resource",
"balance": 1000,
"reason": "seed Kling simulation wallet",
}, http.StatusOK, nil)
var platform struct {
ID string `json:"id"`
}
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms", loginResponse.AccessToken, map[string]any{
"provider": "keling",
"platformKey": "keling-simulation-" + suffix,
"name": "Kling Simulation",
"baseUrl": "https://api-beijing.klingai.com/v1",
"authType": "AccessKey-SecretKey",
"credentials": map[string]any{
"accessKey": "legacy-ak",
"secretKey": "legacy-sk",
},
}, http.StatusCreated, &platform)
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms/"+platform.ID+"/models", loginResponse.AccessToken, map[string]any{
"canonicalModelKey": "keling:kling-3.0-turbo",
"modelName": "kling-3.0-turbo",
"providerModelName": "kling-3.0-turbo",
"modelAlias": "可灵3.0 Turbo",
"modelType": []string{"video_generate", "image_to_video"},
"displayName": "可灵3.0 Turbo",
}, http.StatusCreated, nil)
assertKeling30TurboSimulationTask := func(
name string,
request map[string]any,
expectedModelType string,
) {
t.Helper()
t.Run(name, func(t *testing.T) {
var response struct {
Task struct {
ID string `json:"id"`
Status string `json:"status"`
RunMode string `json:"runMode"`
ModelType string `json:"modelType"`
ResolvedModel string `json:"resolvedModel"`
Result map[string]any `json:"result"`
Metrics map[string]any `json:"metrics"`
BillingSummary map[string]any `json:"billingSummary"`
FinalChargeAmount float64 `json:"finalChargeAmount"`
ResponseDurationMS int64 `json:"responseDurationMs"`
} `json:"task"`
}
doJSON(
t,
server.URL,
http.MethodPost,
"/api/v1/videos/generations",
apiKeyResponse.Secret,
request,
http.StatusAccepted,
&response,
)
task := response.Task
if task.ID == "" ||
task.Status != "succeeded" ||
task.RunMode != "simulation" ||
task.ModelType != expectedModelType ||
task.ResolvedModel != "kling-3.0-turbo" {
t.Fatalf("unexpected Kling simulation task: %+v", task)
}
data, _ := task.Result["data"].([]any)
item, _ := data[0].(map[string]any)
if item["video_url"] != "/static/simulation/video.mp4" ||
item["assetSource"] != "simulation" {
t.Fatalf("unexpected Kling simulation result: %+v", task.Result)
}
if task.FinalChargeAmount <= 0 ||
task.BillingSummary["finalCharge"] == nil ||
task.Metrics["parameterPreprocessingSummary"] == nil ||
task.ResponseDurationMS <= 0 {
t.Fatalf("Kling simulation should preserve billing, preprocessing and timing: %+v", task)
}
})
}
assertKeling30TurboSimulationTask("text-to-video", map[string]any{
"model": "可灵3.0 Turbo",
"runMode": "simulation",
"simulation": true,
"simulationDurationMs": 5,
"prompt": "A cinematic city reveal",
"duration": 8,
"resolution": "1080p",
"aspect_ratio": "9:16",
"audio": true,
}, "video_generate")
assertKeling30TurboSimulationTask("image-to-video", map[string]any{
"model": "可灵3.0 Turbo",
"runMode": "simulation",
"simulation": true,
"simulationDurationMs": 5,
"prompt": "The subject looks toward the camera",
"image": "https://example.com/first.png",
"duration": 5,
"resolution": "720p",
"audio": true,
}, "image_to_video")
}
+97 -29
View File
@@ -16,6 +16,17 @@ type ReadyResponse struct {
OK bool `json:"ok" example:"true"`
}
type SkillBundleMetadataResponse struct {
Name string `json:"name" example:"ai-gateway-ops-management"`
Version string `json:"version" example:"1.0.2"`
DisplayName string `json:"displayName" example:"AI Gateway 运维管理"`
Modules []string `json:"modules" example:"model-runtime"`
FileName string `json:"fileName" example:"ai-gateway-ops-management-v1.0.2.zip"`
DownloadPath string `json:"downloadPath" example:"/api/v1/public/skills/ai-gateway-ops-management/download"`
APIDocsJSONPath string `json:"apiDocsJsonPath" example:"/api-docs-json"`
APIDocsYAMLPath string `json:"apiDocsYamlPath" example:"/api-docs-yaml"`
}
type ErrorEnvelope struct {
Error ErrorPayload `json:"error"`
}
@@ -24,6 +35,8 @@ type ErrorPayload struct {
Message string `json:"message" example:"invalid json body"`
Status int `json:"status" example:"400"`
Code string `json:"code,omitempty" example:"rate_limit"`
Type string `json:"type,omitempty" example:"invalid_request_error"`
Param any `json:"param,omitempty"`
}
type AuthResponse struct {
@@ -182,16 +195,19 @@ type PricingEstimateResponse struct {
type TaskRequest struct {
Model string `json:"model" example:"gpt-4o-mini"`
Messages []ChatMessage `json:"messages,omitempty"`
Input string `json:"input,omitempty" example:"Tell me a short story"`
Input interface{} `json:"input,omitempty"`
Prompt string `json:"prompt,omitempty" example:"A watercolor robot reading a book"`
Text string `json:"text,omitempty" example:"Hello from EasyAI audio synthesis."`
TextFileID string `json:"text_file_id,omitempty" example:""`
VoiceID string `json:"voice_id,omitempty" example:"female-shaonv"`
Stream bool `json:"stream,omitempty" example:"false"`
Stream *bool `json:"stream,omitempty" example:"false"`
RunMode string `json:"runMode,omitempty" example:"simulation"`
MaxTokens int `json:"max_tokens,omitempty" example:"512"`
// ReasoningEffort 推理强度,OpenAI-compatible 请求字段;仅支持 none、minimal、low、medium、high、xhigh。供应商自定义取值由网关按平台适配。
ReasoningEffort string `json:"reasoning_effort,omitempty" example:"medium"`
MaxTokens *int `json:"max_tokens,omitempty" example:"512"`
// MaxCompletionTokens includes visible output and reasoning tokens.
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty" example:"512"`
MaxOutputTokens *int `json:"max_output_tokens,omitempty" example:"512"`
// ReasoningEffort 推理强度,OpenAI-compatible 请求字段;支持 none、minimal、low、medium、high、xhigh、max。供应商自定义取值由网关按平台适配。
ReasoningEffort string `json:"reasoning_effort,omitempty" example:"medium" enums:"none,minimal,low,medium,high,xhigh,max"`
Size string `json:"size,omitempty" example:"1024x1024"`
Duration int `json:"duration,omitempty" example:"5"`
Resolution string `json:"resolution,omitempty" example:"720p"`
@@ -212,36 +228,88 @@ type TaskRequest struct {
}
type ChatCompletionRequest struct {
Model string `json:"model" example:"gpt-4o-mini"`
Messages []ChatMessage `json:"messages"`
Temperature float64 `json:"temperature,omitempty" example:"0.7"`
MaxTokens int `json:"max_tokens,omitempty" example:"512"`
// ReasoningEffort 推理强度,OpenAI-compatible 请求字段;仅支持 none、minimal、low、medium、high、xhigh。供应商自定义取值由网关按平台适配。
ReasoningEffort string `json:"reasoning_effort,omitempty" example:"medium"`
Stream bool `json:"stream,omitempty" example:"false"`
RunMode string `json:"runMode,omitempty" example:"simulation"`
Model string `json:"model" example:"gpt-4o-mini"`
Messages []ChatMessage `json:"messages"`
Audio map[string]interface{} `json:"audio,omitempty"`
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty" example:"0"`
FunctionCall interface{} `json:"function_call,omitempty"`
Functions []map[string]interface{} `json:"functions,omitempty"`
LogitBias map[string]interface{} `json:"logit_bias,omitempty"`
Logprobs *bool `json:"logprobs,omitempty"`
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty" example:"512"`
MaxTokens *int `json:"max_tokens,omitempty" example:"512"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Modalities []string `json:"modalities,omitempty"`
Moderation interface{} `json:"moderation,omitempty"`
N *int `json:"n,omitempty" example:"1"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
Prediction interface{} `json:"prediction,omitempty"`
PresencePenalty *float64 `json:"presence_penalty,omitempty" example:"0"`
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
PromptCacheOptions map[string]interface{} `json:"prompt_cache_options,omitempty"`
PromptCacheRetention string `json:"prompt_cache_retention,omitempty" enums:"in_memory,24h"`
// ReasoningEffort 推理强度,OpenAI-compatible 请求字段;支持 none、minimal、low、medium、high、xhigh、max。供应商自定义取值由网关按平台适配。
ReasoningEffort string `json:"reasoning_effort,omitempty" example:"medium" enums:"none,minimal,low,medium,high,xhigh,max"`
ResponseFormat interface{} `json:"response_format,omitempty"`
SafetyIdentifier string `json:"safety_identifier,omitempty"`
Seed *int `json:"seed,omitempty"`
ServiceTier string `json:"service_tier,omitempty"`
Stop interface{} `json:"stop,omitempty"`
Store *bool `json:"store,omitempty"`
Stream *bool `json:"stream,omitempty" example:"false"`
StreamOptions map[string]interface{} `json:"stream_options,omitempty"`
Temperature *float64 `json:"temperature,omitempty" example:"0.7"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
Tools []map[string]interface{} `json:"tools,omitempty"`
TopLogprobs *int `json:"top_logprobs,omitempty"`
TopP *float64 `json:"top_p,omitempty" example:"1"`
User string `json:"user,omitempty"`
Verbosity string `json:"verbosity,omitempty"`
WebSearchOptions map[string]interface{} `json:"web_search_options,omitempty"`
RunMode string `json:"runMode,omitempty" example:"simulation"`
}
type ChatMessage struct {
Role string `json:"role" example:"user"`
Content string `json:"content" example:"Hello"`
Role string `json:"role" example:"user"`
Content interface{} `json:"content,omitempty"`
Name string `json:"name,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolCalls interface{} `json:"tool_calls,omitempty"`
FunctionCall interface{} `json:"function_call,omitempty"`
}
type ResponsesRequest struct {
Model string `json:"model" example:"Doubao Seed 2.0 Pro"`
Input interface{} `json:"input"`
Instructions string `json:"instructions,omitempty" example:"Answer concisely"`
PreviousResponseID string `json:"previous_response_id,omitempty" example:"resp_0123456789abcdef0123456789abcdef"`
Tools []map[string]interface{} `json:"tools,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
ParallelToolCalls bool `json:"parallel_tool_calls,omitempty" example:"true"`
MaxOutputTokens int `json:"max_output_tokens,omitempty" example:"512"`
Reasoning map[string]interface{} `json:"reasoning,omitempty"`
Text map[string]interface{} `json:"text,omitempty"`
Temperature float64 `json:"temperature,omitempty" example:"0.7"`
TopP float64 `json:"top_p,omitempty" example:"1"`
Store *bool `json:"store,omitempty"`
Stream bool `json:"stream,omitempty" example:"false"`
Model string `json:"model" example:"Doubao Seed 2.0 Pro"`
Background *bool `json:"background,omitempty"`
ContextManagement []map[string]interface{} `json:"context_management,omitempty"`
Conversation interface{} `json:"conversation,omitempty"`
Include []string `json:"include,omitempty"`
Input interface{} `json:"input"`
Instructions string `json:"instructions,omitempty" example:"Answer concisely"`
MaxOutputTokens *int `json:"max_output_tokens,omitempty" example:"512"`
MaxToolCalls *int `json:"max_tool_calls,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Moderation interface{} `json:"moderation,omitempty"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty" example:"true"`
PreviousResponseID string `json:"previous_response_id,omitempty" example:"resp_0123456789abcdef0123456789abcdef"`
Prompt interface{} `json:"prompt,omitempty"`
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
PromptCacheOptions map[string]interface{} `json:"prompt_cache_options,omitempty"`
PromptCacheRetention string `json:"prompt_cache_retention,omitempty" enums:"in_memory,24h"`
Reasoning map[string]interface{} `json:"reasoning,omitempty"`
SafetyIdentifier string `json:"safety_identifier,omitempty"`
ServiceTier string `json:"service_tier,omitempty"`
Store *bool `json:"store,omitempty"`
Stream *bool `json:"stream,omitempty" example:"false"`
StreamOptions map[string]interface{} `json:"stream_options,omitempty"`
Temperature *float64 `json:"temperature,omitempty" example:"0.7"`
Text map[string]interface{} `json:"text,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
Tools []map[string]interface{} `json:"tools,omitempty"`
TopLogprobs *int `json:"top_logprobs,omitempty"`
TopP *float64 `json:"top_p,omitempty" example:"1"`
Truncation string `json:"truncation,omitempty"`
User string `json:"user,omitempty"`
}
type ResponsesCompatibleResponse struct {
+6
View File
@@ -25,6 +25,12 @@ func writeErrorWithDetails(w http.ResponseWriter, status int, message string, de
if len(codes) > 0 {
if code := strings.TrimSpace(codes[0]); code != "" {
errorPayload["code"] = code
if code == "invalid_parameter" || code == "unsupported_response_parameter" {
errorPayload["type"] = "invalid_request_error"
if _, ok := details["param"]; !ok {
errorPayload["param"] = nil
}
}
}
}
for key, value := range details {
+4
View File
@@ -128,6 +128,10 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
mux.HandleFunc("GET /static/simulation/{asset}", serveSimulationAsset)
mux.HandleFunc("GET /static/generated/{asset}", server.serveGeneratedStaticAsset)
mux.HandleFunc("GET /static/uploaded/{asset}", server.serveUploadedStaticAsset)
mux.HandleFunc("GET /api-docs-json", server.apiDocsJSON)
mux.HandleFunc("GET /api-docs-yaml", server.apiDocsYAML)
mux.HandleFunc("GET /api/v1/public/skills/ai-gateway-ops-management/metadata", server.getOpsManagementSkillMetadata)
mux.HandleFunc("GET /api/v1/public/skills/ai-gateway-ops-management/download", server.downloadOpsManagementSkill)
mux.Handle("POST /api/v1/auth/register", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.register)))
mux.Handle("POST /api/v1/auth/login", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.login)))