feat:Add error handling and informative messages to the Zhipu AI provider response structure, and create unit tests to validate the functionality.

This commit is contained in:
dashen
2024-12-26 14:55:43 +08:00
parent fb9c93c724
commit 9620891764
4 changed files with 305 additions and 4 deletions

View File

@@ -0,0 +1 @@
/.env

View File

@@ -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"`
}

View File

@@ -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 {

View File

@@ -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)
}