From 96208917649c70b53ac37d7e224da8f15d93d74f Mon Sep 17 00:00:00 2001 From: dashen <2944321442@qq.com> Date: Thu, 26 Dec 2024 14:55:43 +0800 Subject: [PATCH] feat:Add error handling and informative messages to the Zhipu AI provider response structure, and create unit tests to validate the functionality. --- drivers/ai-provider/zhipuai/.gitignore | 1 + drivers/ai-provider/zhipuai/message.go | 10 + drivers/ai-provider/zhipuai/mode.go | 90 ++++++++- drivers/ai-provider/zhipuai/zhipuai_test.go | 208 ++++++++++++++++++++ 4 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 drivers/ai-provider/zhipuai/.gitignore create mode 100644 drivers/ai-provider/zhipuai/zhipuai_test.go diff --git a/drivers/ai-provider/zhipuai/.gitignore b/drivers/ai-provider/zhipuai/.gitignore new file mode 100644 index 00000000..f10862a6 --- /dev/null +++ b/drivers/ai-provider/zhipuai/.gitignore @@ -0,0 +1 @@ +/.env diff --git a/drivers/ai-provider/zhipuai/message.go b/drivers/ai-provider/zhipuai/message.go index 4dadc653..9eb16570 100644 --- a/drivers/ai-provider/zhipuai/message.go +++ b/drivers/ai-provider/zhipuai/message.go @@ -17,6 +17,7 @@ type Response struct { SystemFingerprint string `json:"system_fingerprint"` Choices []ResponseChoice `json:"choices"` Usage Usage `json:"usage"` + Error Error `json:"error"` } type ResponseChoice struct { @@ -26,6 +27,8 @@ type ResponseChoice struct { FinishReason string `json:"finish_reason"` } +// Usage provides information about the token counts for the request and response. +// {"completion_tokens":217,"prompt_tokens":31,"total_tokens":248} type Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` @@ -36,3 +39,10 @@ type Usage struct { type CompletionTokensDetails struct { ReasoningTokens int `json:"reasoning_tokens"` } + +// Error represents an error response from the API. +// {"error":{"code":"1002","message":"Authorization Token非法,请确认Authorization Token正确传递。"}} +type Error struct { + Message string `json:"message"` + Code string `json:"code"` +} diff --git a/drivers/ai-provider/zhipuai/mode.go b/drivers/ai-provider/zhipuai/mode.go index efe68fe6..cb7ad1bb 100644 --- a/drivers/ai-provider/zhipuai/mode.go +++ b/drivers/ai-provider/zhipuai/mode.go @@ -75,15 +75,97 @@ func (c *Chat) ResponseConvert(ctx eocontext.EoContext) error { if err != nil { return err } - if httpContext.Response().StatusCode() != 200 { - return nil - } body := httpContext.Response().GetBody() data := eosc.NewBase[Response]() err = json.Unmarshal(body, data) if err != nil { return err } + // http status code + // 200 - 业务处理成功 + // 400 - 参数错误或文件内容异常 + // 401 - 认证失败或Token超时 + // 404 - 微调功能不可用或微调任务不存在 + // 429 - 接口请求并发超限、文件上传频率过快、账户余额耗尽、账户异常或终端账户异常 + // 434 - 无API权限,微调API和文件管理API处于Beta阶段 + // 435 - 文件大小超过100MB + // 500 - 服务器在处理请求时发生错误 + switch httpContext.Response().StatusCode() { + case 200: + // Calculate the token consumption for a successful request. + usage := data.Config.Usage + ai_provider.SetAIStatusNormal(ctx) + ai_provider.SetAIModelInputToken(ctx, usage.PromptTokens) + ai_provider.SetAIModelOutputToken(ctx, usage.CompletionTokens) + ai_provider.SetAIModelTotalToken(ctx, usage.TotalTokens) + case 429: + // 业务错误码汇总 + // 基本错误 + // 500 - 内部错误 + // 认证错误 + // 1000 - 认证失败 + // 1001 - Header中未接收到认证参数,无法认证 + // 1002 - 无效的认证Token,请确认认证Token的正确传递 + // 1003 - 认证Token已过期,请重新生成/获取 + // 1004 - 提供的认证Token认证失败 + // 账户错误 + // 1100 - 账户读写 + // 1110 - 账户当前处于非活跃状态,请检查账户信息 + // 1111 - 账户不存在 + // 1112 - 账户已被锁定,请与客服联系解锁 + // 1113 - 账户欠费,请充值后重试 + // 1120 - 无法成功访问账户,请稍后再试 + // API调用错误 + // 1200 - API调用错误 + // 1210 - API调用参数不正确,请检查文档 + // 1211 - 模型不存在,请检查模型代码 + // 1212 - 当前模型不支持${method}调用方法 + // 1213 - ${field}参数未正确接收 + // 1214 - ${field}参数无效,请检查文档 + // 1215 - ${field1}和${field2}不能同时设置,请检查文档 + // 1220 - 您没有权限访问${API_name} + // 1221 - API ${API_name}已下线 + // 1222 - API ${API_name}不存在 + // 1230 - API调用过程错误 + // 1231 - 您已有请求:${request_id} + // 1232 - 获取异步请求结果时,请使用task_id + // 1233 - 任务:${task_id}不存在 + // 1234 - 网络错误,错误id:${error_id},请与客服联系 + // 1235 - 网络错误,错误id:${error_id},请与客服联系 + // 1260 - API运行时错误 + // 1261 - 提示过长 + // API策略阻断错误 + // 1300 - API调用被策略阻断 + // 1301 - 系统检测到输入或生成中可能存在不安全或敏感内容,请避免使用可能生成敏感内容的提示 + // 1302 - 此API的高并发使用,请降低并发或联系客服提高限制 + // 1303 - 此API的高频率使用,请降低频率或联系客服提高限制 + // 1304 - 此API的日调用限额已达到,如需更多请求,请与客服联系购买 + // 1305 - 目前API请求过多,请稍后再试 + switch data.Config.Error.Code { + case "1001", "1002", "1003", "1004": + // Handle the auth error. + ai_provider.SetAIStatusInvalid(ctx) + case "1110", "1111", "1112", "1113", "1120": + // Handle the account error. + ai_provider.SetAIStatusQuotaExhausted(ctx) + case "1302", "1303", "1304", "1305": + // Handle the rate limit error. + ai_provider.SetAIStatusExceeded(ctx) + default: + // Handle the bad request error. + ai_provider.SetAIStatusInvalidRequest(ctx) + } + case 401: + // Handle the auth error. + ai_provider.SetAIStatusInvalid(ctx) + case 400: + // Handle the bad request error. + ai_provider.SetAIStatusInvalidRequest(ctx) + default: + // Handle the bad request error. + ai_provider.SetAIStatusInvalidRequest(ctx) + + } responseBody := &ai_provider.ClientResponse{} if len(data.Config.Choices) > 0 { msg := data.Config.Choices[0] @@ -94,7 +176,7 @@ func (c *Chat) ResponseConvert(ctx eocontext.EoContext) error { responseBody.FinishReason = msg.FinishReason } else { responseBody.Code = -1 - responseBody.Error = "no response" + responseBody.Error = data.Config.Error.Message } body, err = json.Marshal(responseBody) if err != nil { diff --git a/drivers/ai-provider/zhipuai/zhipuai_test.go b/drivers/ai-provider/zhipuai/zhipuai_test.go new file mode 100644 index 00000000..04b13578 --- /dev/null +++ b/drivers/ai-provider/zhipuai/zhipuai_test.go @@ -0,0 +1,208 @@ +package zhipuai + +import ( + "fmt" + "net/url" + "os" + "testing" + "time" + + "github.com/eolinker/apinto/convert" + ai_provider "github.com/eolinker/apinto/drivers/ai-provider" + http_context "github.com/eolinker/apinto/node/http-context" + "github.com/joho/godotenv" + "github.com/valyala/fasthttp" +) + +var ( + defaultConfig = `{ + "frequency_penalty": "", + "max_tokens": 512, + "presence_penalty": "", + "response_format": "", + "temperature": "", + "top_p": "" + }` + successBody = []byte(`{ + "messages": [ + { + "content": "作为一名营销专家,请为我的产品创作一个吸引人的口号", + "role": "user" + } + ] + }`) + failBody = []byte(`{ + "messages": [ + { + "content": "作为一名营销专家,请为我的产品创作一个吸引人的口号", + "role": "assistant" + } + ],"variables":{} + }`) +) + +// TestSentTo tests the end-to-end execution of the zhipu integration. +func TestSentTo(t *testing.T) { + // Load .env file + err := godotenv.Load(".env") + if err != nil { + t.Fatalf("Error loading .env file: %v", err) + } + + // Test data for different scenarios + testData := []struct { + name string + apiKey string + wantStatus string + body []byte + }{ + { + name: "success", + apiKey: os.Getenv("ValidKey"), + wantStatus: ai_provider.StatusNormal, + body: successBody, + }, + { + name: "invalid request", + apiKey: os.Getenv("ValidKey"), + wantStatus: ai_provider.StatusInvalidRequest, + body: failBody, + }, + { + name: "invalid key", + apiKey: os.Getenv("InvalidKey"), + wantStatus: ai_provider.StatusInvalid, + }, + { + name: "expired key", + apiKey: os.Getenv("ExpiredKey"), + wantStatus: ai_provider.StatusInvalid, + }, + } + + // Run tests for each scenario + for _, data := range testData { + t.Run(data.name, func(t *testing.T) { + if err := runTest(data.apiKey, data.body, data.wantStatus); err != nil { + t.Fatalf("Test failed: %v", err) + } + }) + } +} + +// runTest handles a single test case +func runTest(apiKey string, requestBody []byte, wantStatus string) error { + cfg := &Config{ + APIKey: apiKey, + Organization: "", + } + + // Create the worker + worker, err := Create("zhipuai", "zhipuai", cfg, nil) + if err != nil { + return fmt.Errorf("failed to create worker: %w", err) + } + + // Get the handler + handler, ok := worker.(convert.IConverterDriver) + if !ok { + return fmt.Errorf("worker does not implement IConverterDriver") + } + + // Default to success body if no body is provided + if len(requestBody) == 0 { + requestBody = successBody + } + + // Mock HTTP context + ctx := createMockHttpContext("/xxx/xxx", nil, nil, requestBody) + + // Execute the conversion process + err = executeConverter(ctx, handler, "glm-4v", "https://open.bigmodel.cn") + if err != nil { + return fmt.Errorf("failed to execute conversion process: %w", err) + } + // Check the status + if ai_provider.GetAIStatus(ctx) != wantStatus { + return fmt.Errorf("unexpected status: got %s, expected %s", ai_provider.GetAIStatus(ctx), wantStatus) + } + + return nil +} + +// executeConverter handles the full flow of a conversion process. +func executeConverter(ctx *http_context.HttpContext, handler convert.IConverterDriver, model string, baseUrl string) error { + // Balance handler setup + balanceHandler, err := ai_provider.NewBalanceHandler("test", baseUrl, 30*time.Second) + if err != nil { + return fmt.Errorf("failed to create balance handler: %w", err) + } + + // Get model function + fn, has := handler.GetModel(model) + if !has { + return fmt.Errorf("model %s not found", model) + } + + // Generate config + extender, err := fn(defaultConfig) + if err != nil { + return fmt.Errorf("failed to generate config: %w", err) + } + + // Get converter + converter, has := handler.GetConverter(model) + if !has { + return fmt.Errorf("converter for model %s not found", model) + } + + // Convert request + if err := converter.RequestConvert(ctx, extender); err != nil { + return fmt.Errorf("request conversion failed: %w", err) + } + + // Select node via balance handler + node, _, err := balanceHandler.Select(ctx) + if err != nil { + return fmt.Errorf("node selection failed: %w", err) + } + + // Send request to the node + if err := ctx.SendTo(balanceHandler.Scheme(), node, balanceHandler.TimeOut()); err != nil { + return fmt.Errorf("failed to send request to node: %w", err) + } + + // Convert response + if err := converter.ResponseConvert(ctx); err != nil { + return fmt.Errorf("response conversion failed: %w", err) + } + + return nil +} + +// createMockHttpContext creates a mock fasthttp.RequestCtx and wraps it with HttpContext. +func createMockHttpContext(rawURL string, headers map[string]string, query url.Values, body []byte) *http_context.HttpContext { + req := fasthttp.AcquireRequest() + u := fasthttp.AcquireURI() + + // Set request URI and path + uri, _ := url.Parse(rawURL) + u.SetPath(uri.Path) + u.SetScheme(uri.Scheme) + u.SetHost(uri.Host) + u.SetQueryString(uri.RawQuery) + req.SetURI(u) + req.Header.SetMethod("POST") + + // Set headers + for k, v := range headers { + req.Header.Set(k, v) + } + req.SetBody(body) + + // Create HttpContext + return http_context.NewContext(&fasthttp.RequestCtx{ + Request: *req, + Response: fasthttp.Response{}, + }, 8099) +}