mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-07 00:17:37 +08:00
feat: implement multi-model service configuration support
- Support configuring multiple LLM services simultaneously - Auto-derive model names from service types to simplify configuration - Maintain backward compatibility with existing configurations - Refactor configuration logic into dedicated env module - Add comprehensive unit test coverage - Update documentation with new configuration approach
This commit is contained in:
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2506061529
|
v5.0.0-beta-2506062217
|
||||||
|
|||||||
+105
-14
@@ -103,8 +103,8 @@ type ModelConfig struct {
|
|||||||
- 多模型类型支持
|
- 多模型类型支持
|
||||||
|
|
||||||
**支持的模型类型**:
|
**支持的模型类型**:
|
||||||
- `LLMServiceTypeUITARS`: UI-TARS 专业 UI 自动化模型
|
- `DOUBAO_1_5_THINKING_VISION_PRO_250428`: 豆包思维视觉专业版
|
||||||
- `LLMServiceTypeDoubaoVL`: 豆包视觉语言模型
|
- `DOUBAO_1_5_UI_TARS_250428`: 豆包UI-TARS专业UI自动化模型
|
||||||
|
|
||||||
### 2. 智能规划器 (planner.go)
|
### 2. 智能规划器 (planner.go)
|
||||||
|
|
||||||
@@ -290,30 +290,120 @@ type ConversationHistory []*schema.Message
|
|||||||
|
|
||||||
### 1. 环境配置
|
### 1. 环境配置
|
||||||
|
|
||||||
设置必要的环境变量:
|
HttpRunner AI 模块支持多模型服务配置,您可以同时配置多个大模型服务,然后在测试用例中灵活切换。
|
||||||
|
|
||||||
|
#### 多模型配置方式
|
||||||
|
|
||||||
|
**服务特定配置**:
|
||||||
|
```bash
|
||||||
|
# 豆包思维视觉专业版配置
|
||||||
|
DOUBAO_1_5_THINKING_VISION_PRO_250428_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||||
|
DOUBAO_1_5_THINKING_VISION_PRO_250428_API_KEY=your_doubao_api_key
|
||||||
|
|
||||||
|
# 豆包UI-TARS配置
|
||||||
|
DOUBAO_1_5_UI_TARS_250428_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||||
|
DOUBAO_1_5_UI_TARS_250428_API_KEY=your_doubao_ui_tars_api_key
|
||||||
|
|
||||||
|
**默认配置(向后兼容)**:
|
||||||
|
```bash
|
||||||
|
# 默认配置,当没有找到服务特定配置时使用
|
||||||
|
LLM_MODEL_NAME=doubao-1.5-thinking-vision-pro-250428
|
||||||
|
OPENAI_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||||
|
OPENAI_API_KEY=your_default_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 环境变量命名规则
|
||||||
|
|
||||||
|
- 将服务名称转换为大写
|
||||||
|
- 将连字符 `-` 和点号 `.` 替换为下划线 `_`
|
||||||
|
- 添加对应的后缀:`_BASE_URL`、`_API_KEY`
|
||||||
|
- 模型名称直接从服务类型推导,无需单独配置
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- `doubao-1.5-thinking-vision-pro-250428` → `DOUBAO_1_5_THINKING_VISION_PRO_250428_*`
|
||||||
|
- `gpt-4` → `GPT_4_*`
|
||||||
|
- `claude-3.5-sonnet` → `CLAUDE_3_5_SONNET_*`
|
||||||
|
|
||||||
|
#### 配置优先级
|
||||||
|
|
||||||
|
1. **服务特定配置**(最高优先级):`{SERVICE_NAME}_BASE_URL`、`{SERVICE_NAME}_API_KEY`
|
||||||
|
2. **默认配置**(向后兼容):`OPENAI_BASE_URL`、`OPENAI_API_KEY`、`LLM_MODEL_NAME`
|
||||||
|
3. **模型名称**:优先使用服务类型名称,仅在完全使用默认配置时才使用 `LLM_MODEL_NAME`
|
||||||
|
|
||||||
|
#### 示例 .env 文件
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export OPENAI_BASE_URL="https://your-api-endpoint"
|
# 默认配置
|
||||||
export OPENAI_API_KEY="your-api-key"
|
LLM_MODEL_NAME=doubao-1.5-thinking-vision-pro-250428
|
||||||
export LLM_MODEL_NAME="your-model-name"
|
OPENAI_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||||
|
OPENAI_API_KEY=your_default_api_key
|
||||||
|
|
||||||
|
# doubao-1.5-thinking-vision-pro-250428
|
||||||
|
DOUBAO_1_5_THINKING_VISION_PRO_250428_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||||
|
DOUBAO_1_5_THINKING_VISION_PRO_250428_API_KEY=your_doubao_thinking_api_key
|
||||||
|
|
||||||
|
# doubao-1.5-ui-tars-250428
|
||||||
|
DOUBAO_1_5_UI_TARS_250428_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||||
|
DOUBAO_1_5_UI_TARS_250428_API_KEY=your_doubao_ui_tars_api_key
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 创建 LLM 服务
|
### 2. 创建 LLM 服务
|
||||||
|
|
||||||
|
#### 在测试用例中指定服务
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "AI测试用例",
|
||||||
|
"llm_service": "doubao-1.5-thinking-vision-pro-250428"
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "AI操作步骤",
|
||||||
|
"android": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "start_to_goal",
|
||||||
|
"params": "启动应用并完成某个任务"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 在Go代码中使用
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 创建 UI-TARS 服务
|
// 创建豆包思维视觉专业版服务
|
||||||
llmService, err := ai.NewLLMService(option.LLMServiceTypeUITARS)
|
llmService, err := ai.NewLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to create LLM service")
|
log.Fatal().Err(err).Msg("failed to create LLM service")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建豆包视觉服务
|
// 创建豆包UI-TARS服务
|
||||||
llmService, err := ai.NewLLMService(option.LLMServiceTypeDoubaoVL)
|
llmService, err := ai.NewLLMService(option.DOUBAO_1_5_UI_TARS_250428)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to create LLM service")
|
log.Fatal().Err(err).Msg("failed to create LLM service")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 模型切换
|
||||||
|
|
||||||
|
要切换到不同的模型服务,只需要修改测试用例中的 `llm_service` 字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "连连看游戏测试",
|
||||||
|
"llm_service": "doubao-1.5-ui-tars-250428"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
系统会自动根据服务名称获取对应的配置,无需修改环境变量。
|
||||||
|
|
||||||
### 3. 智能规划使用
|
### 3. 智能规划使用
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -446,8 +536,8 @@ log.Info().Float64("x", center.X).Float64("y", center.Y).
|
|||||||
|
|
||||||
AI 模块支持多种不同的语言模型,每种模型都有其特定的优势:
|
AI 模块支持多种不同的语言模型,每种模型都有其特定的优势:
|
||||||
|
|
||||||
- **UI-TARS**: 专门针对 UI 自动化优化的模型,支持 Thought/Action 格式
|
- **豆包思维视觉专业版**: 支持深度思考的视觉语言模型,适合复杂场景分析
|
||||||
- **豆包视觉**: 通用视觉语言模型,支持结构化 JSON 输出
|
- **豆包UI-TARS**: 专门针对 UI 自动化优化的模型,支持 Thought/Action 格式
|
||||||
|
|
||||||
### 2. 坐标系统转换
|
### 2. 坐标系统转换
|
||||||
|
|
||||||
@@ -495,7 +585,8 @@ func normalizeParameterName(paramName string) string {
|
|||||||
### 1. 环境变量配置
|
### 1. 环境变量配置
|
||||||
- 确保所有必需的环境变量都已正确设置
|
- 确保所有必需的环境变量都已正确设置
|
||||||
- API 密钥需要有足够的权限和配额
|
- API 密钥需要有足够的权限和配额
|
||||||
- 模型名称必须与服务类型匹配
|
- 支持多模型配置,可以同时配置多个服务
|
||||||
|
- 模型名称自动从服务类型推导,无需手动配置
|
||||||
|
|
||||||
### 2. 图像格式要求
|
### 2. 图像格式要求
|
||||||
- 支持 Base64 编码的图像数据
|
- 支持 Base64 编码的图像数据
|
||||||
@@ -503,7 +594,7 @@ func normalizeParameterName(paramName string) string {
|
|||||||
- 图像尺寸信息必须准确提供
|
- 图像尺寸信息必须准确提供
|
||||||
|
|
||||||
### 3. 坐标系统
|
### 3. 坐标系统
|
||||||
- UI-TARS 使用 1000x1000 相对坐标系统
|
- 豆包UI-TARS 使用 1000x1000 相对坐标系统
|
||||||
- 需要正确的屏幕尺寸信息进行坐标转换
|
- 需要正确的屏幕尺寸信息进行坐标转换
|
||||||
- 注意不同模型的坐标格式差异
|
- 注意不同模型的坐标格式差异
|
||||||
|
|
||||||
|
|||||||
-106
@@ -2,17 +2,8 @@ package ai
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cloudwego/eino-ext/components/model/openai"
|
|
||||||
"github.com/httprunner/httprunner/v5/code"
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/config"
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ILLMService 定义了 LLM 服务接口,包括规划和断言功能
|
// ILLMService 定义了 LLM 服务接口,包括规划和断言功能
|
||||||
@@ -58,100 +49,3 @@ func (c *combinedLLMService) Call(ctx context.Context, opts *PlanningOptions) (*
|
|||||||
func (c *combinedLLMService) Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error) {
|
func (c *combinedLLMService) Assert(ctx context.Context, opts *AssertOptions) (*AssertionResult, error) {
|
||||||
return c.asserter.Assert(ctx, opts)
|
return c.asserter.Assert(ctx, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LLM model config env variables
|
|
||||||
const (
|
|
||||||
EnvOpenAIBaseURL = "OPENAI_BASE_URL"
|
|
||||||
EnvOpenAIAPIKey = "OPENAI_API_KEY"
|
|
||||||
EnvModelName = "LLM_MODEL_NAME"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultTimeout = 30 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
type ModelConfig struct {
|
|
||||||
*openai.ChatModelConfig
|
|
||||||
ModelType option.LLMServiceType
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetModelConfig get OpenAI config
|
|
||||||
func GetModelConfig(modelType option.LLMServiceType) (*ModelConfig, error) {
|
|
||||||
if err := config.LoadEnv(); err != nil {
|
|
||||||
return nil, errors.Wrap(code.LoadEnvError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
openaiBaseURL := os.Getenv(EnvOpenAIBaseURL)
|
|
||||||
if openaiBaseURL == "" {
|
|
||||||
return nil, errors.Wrapf(code.LLMEnvMissedError,
|
|
||||||
"env %s missed", EnvOpenAIBaseURL)
|
|
||||||
}
|
|
||||||
openaiAPIKey := os.Getenv(EnvOpenAIAPIKey)
|
|
||||||
if openaiAPIKey == "" {
|
|
||||||
return nil, errors.Wrapf(code.LLMEnvMissedError,
|
|
||||||
"env %s missed", EnvOpenAIAPIKey)
|
|
||||||
}
|
|
||||||
modelName := os.Getenv(EnvModelName)
|
|
||||||
if modelName == "" {
|
|
||||||
return nil, errors.Wrapf(code.LLMEnvMissedError,
|
|
||||||
"env %s missed", EnvModelName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate model type and model name compatibility
|
|
||||||
if err := validateModelType(modelType, modelName); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://www.volcengine.com/docs/82379/1536429
|
|
||||||
temperature := float32(0)
|
|
||||||
topP := float32(0.7)
|
|
||||||
modelConfig := &openai.ChatModelConfig{
|
|
||||||
BaseURL: openaiBaseURL,
|
|
||||||
APIKey: openaiAPIKey,
|
|
||||||
Model: modelName,
|
|
||||||
Timeout: defaultTimeout,
|
|
||||||
Temperature: &temperature,
|
|
||||||
TopP: &topP,
|
|
||||||
}
|
|
||||||
|
|
||||||
// log config info
|
|
||||||
log.Info().Str("model", modelConfig.Model).
|
|
||||||
Str("baseURL", modelConfig.BaseURL).
|
|
||||||
Str("apiKey", maskAPIKey(modelConfig.APIKey)).
|
|
||||||
Str("timeout", defaultTimeout.String()).
|
|
||||||
Msg("get model config")
|
|
||||||
|
|
||||||
return &ModelConfig{
|
|
||||||
ChatModelConfig: modelConfig,
|
|
||||||
ModelType: modelType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateModelType(modelType option.LLMServiceType, modelName string) error {
|
|
||||||
switch modelType {
|
|
||||||
case option.DOUBAO_1_5_UI_TARS_250428:
|
|
||||||
if !strings.Contains(modelName, string(modelType)) {
|
|
||||||
return fmt.Errorf("model name %s is not supported for %s", modelName, modelType)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case option.DOUBAO_1_5_THINKING_VISION_PRO_250428:
|
|
||||||
if !strings.Contains(modelName, string(modelType)) {
|
|
||||||
return fmt.Errorf("model name %s is not supported", modelName)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("model type %s is not supported, supported types: %s, %s",
|
|
||||||
modelType,
|
|
||||||
option.DOUBAO_1_5_UI_TARS_250428,
|
|
||||||
option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
|
||||||
}
|
|
||||||
|
|
||||||
// maskAPIKey masks the API key
|
|
||||||
func maskAPIKey(key string) string {
|
|
||||||
if len(key) <= 8 {
|
|
||||||
return "******"
|
|
||||||
}
|
|
||||||
|
|
||||||
return key[:4] + "******" + key[len(key)-4:]
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -133,7 +133,7 @@ Here is the assertion. Please tell whether it is truthy according to the screens
|
|||||||
logRequest(a.history)
|
logRequest(a.history)
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
message, err := a.model.Generate(ctx, a.history)
|
message, err := a.model.Generate(ctx, a.history)
|
||||||
log.Info().Float64("elapsed(s)", time.Since(startTime).Seconds()).
|
log.Debug().Float64("elapsed(s)", time.Since(startTime).Seconds()).
|
||||||
Str("model", string(a.modelConfig.ModelType)).Msg("call model service for assertion")
|
Str("model", string(a.modelConfig.ModelType)).Msg("call model service for assertion")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.LLMRequestServiceError, err.Error())
|
return nil, errors.Wrap(code.LLMRequestServiceError, err.Error())
|
||||||
|
|||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino-ext/components/model/openai"
|
||||||
|
"github.com/httprunner/httprunner/v5/code"
|
||||||
|
"github.com/httprunner/httprunner/v5/internal/config"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LLM model config env variables
|
||||||
|
const (
|
||||||
|
EnvOpenAIBaseURL = "OPENAI_BASE_URL"
|
||||||
|
EnvOpenAIAPIKey = "OPENAI_API_KEY"
|
||||||
|
EnvModelName = "LLM_MODEL_NAME"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetModelConfig get OpenAI config
|
||||||
|
func GetModelConfig(modelType option.LLMServiceType) (*ModelConfig, error) {
|
||||||
|
if err := config.LoadEnv(); err != nil {
|
||||||
|
return nil, errors.Wrap(code.LoadEnvError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL, apiKey, modelName, err := getModelConfigFromEnv(modelType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(code.LLMEnvMissedError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.volcengine.com/docs/82379/1536429
|
||||||
|
temperature := float32(0)
|
||||||
|
topP := float32(0.7)
|
||||||
|
modelConfig := &openai.ChatModelConfig{
|
||||||
|
BaseURL: baseURL,
|
||||||
|
APIKey: apiKey,
|
||||||
|
Model: modelName,
|
||||||
|
Timeout: defaultTimeout,
|
||||||
|
Temperature: &temperature,
|
||||||
|
TopP: &topP,
|
||||||
|
}
|
||||||
|
|
||||||
|
// log config info
|
||||||
|
log.Info().Str("model", modelConfig.Model).
|
||||||
|
Str("baseURL", modelConfig.BaseURL).
|
||||||
|
Str("apiKey", maskAPIKey(modelConfig.APIKey)).
|
||||||
|
Str("timeout", defaultTimeout.String()).
|
||||||
|
Str("serviceType", string(modelType)).
|
||||||
|
Msg("get model config")
|
||||||
|
|
||||||
|
return &ModelConfig{
|
||||||
|
ChatModelConfig: modelConfig,
|
||||||
|
ModelType: modelType,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelConfig struct {
|
||||||
|
*openai.ChatModelConfig
|
||||||
|
ModelType option.LLMServiceType
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceEnvPrefix converts LLMServiceType to environment variable prefix
|
||||||
|
// e.g., "doubao-1.5-thinking-vision-pro-250428" -> "DOUBAO_1_5_THINKING_VISION_PRO_250428"
|
||||||
|
func getServiceEnvPrefix(modelType option.LLMServiceType) string {
|
||||||
|
// Convert service name to uppercase and replace hyphens and dots with underscores
|
||||||
|
prefix := strings.ToUpper(string(modelType))
|
||||||
|
prefix = strings.ReplaceAll(prefix, "-", "_")
|
||||||
|
prefix = strings.ReplaceAll(prefix, ".", "_")
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// getModelConfigFromEnv retrieves model configuration from environment variables
|
||||||
|
// It first tries to get service-specific config, then falls back to default config
|
||||||
|
// Model name is derived from the service type, no need for separate MODEL_NAME env var
|
||||||
|
func getModelConfigFromEnv(modelType option.LLMServiceType) (baseURL, apiKey, modelName string, err error) {
|
||||||
|
servicePrefix := getServiceEnvPrefix(modelType)
|
||||||
|
|
||||||
|
// Try to get service-specific configuration first
|
||||||
|
baseURL = os.Getenv(servicePrefix + "_BASE_URL")
|
||||||
|
apiKey = os.Getenv(servicePrefix + "_API_KEY")
|
||||||
|
|
||||||
|
// Model name is derived from the service type itself
|
||||||
|
modelName = string(modelType)
|
||||||
|
|
||||||
|
envBaseURL := os.Getenv(EnvOpenAIBaseURL)
|
||||||
|
envAPIKey := os.Getenv(EnvOpenAIAPIKey)
|
||||||
|
|
||||||
|
// If service-specific config is not found, fall back to default config
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = envBaseURL
|
||||||
|
}
|
||||||
|
if apiKey == "" {
|
||||||
|
apiKey = envAPIKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're using default config completely (both base URL and API key from default),
|
||||||
|
// then use default model name if available
|
||||||
|
if baseURL == envBaseURL && apiKey == envAPIKey {
|
||||||
|
defaultModelName := os.Getenv(EnvModelName)
|
||||||
|
if defaultModelName != "" {
|
||||||
|
modelName = defaultModelName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all required configs are available
|
||||||
|
if baseURL == "" {
|
||||||
|
return "", "", "", errors.Errorf("env %s or %s missed", servicePrefix+"_BASE_URL", EnvOpenAIBaseURL)
|
||||||
|
}
|
||||||
|
if apiKey == "" {
|
||||||
|
return "", "", "", errors.Errorf("env %s or %s missed", servicePrefix+"_API_KEY", EnvOpenAIAPIKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseURL, apiKey, modelName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskAPIKey masks the API key
|
||||||
|
func maskAPIKey(key string) string {
|
||||||
|
if len(key) <= 8 {
|
||||||
|
return "******"
|
||||||
|
}
|
||||||
|
|
||||||
|
return key[:4] + "******" + key[len(key)-4:]
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package ai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetServiceEnvPrefix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
modelType option.LLMServiceType
|
||||||
|
expectedPrefix string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "doubao thinking vision pro",
|
||||||
|
modelType: option.DOUBAO_1_5_THINKING_VISION_PRO_250428,
|
||||||
|
expectedPrefix: "DOUBAO_1_5_THINKING_VISION_PRO_250428",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "doubao ui tars",
|
||||||
|
modelType: option.DOUBAO_1_5_UI_TARS_250428,
|
||||||
|
expectedPrefix: "DOUBAO_1_5_UI_TARS_250428",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
prefix := getServiceEnvPrefix(tt.modelType)
|
||||||
|
assert.Equal(t, tt.expectedPrefix, prefix)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetModelConfigFromEnv_ServiceSpecific(t *testing.T) {
|
||||||
|
// Clean up environment variables after test
|
||||||
|
defer func() {
|
||||||
|
os.Unsetenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_BASE_URL")
|
||||||
|
os.Unsetenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_API_KEY")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set service-specific environment variables (no need for MODEL_NAME)
|
||||||
|
os.Setenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_BASE_URL", "https://test-base-url.com")
|
||||||
|
os.Setenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_API_KEY", "test-api-key")
|
||||||
|
|
||||||
|
baseURL, apiKey, modelName, err := getModelConfigFromEnv(option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://test-base-url.com", baseURL)
|
||||||
|
assert.Equal(t, "test-api-key", apiKey)
|
||||||
|
assert.Equal(t, "doubao-1.5-thinking-vision-pro-250428", modelName) // Model name derived from service type
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetModelConfigFromEnv_FallbackToDefault(t *testing.T) {
|
||||||
|
// Clean up environment variables after test
|
||||||
|
defer func() {
|
||||||
|
os.Unsetenv("OPENAI_BASE_URL")
|
||||||
|
os.Unsetenv("OPENAI_API_KEY")
|
||||||
|
os.Unsetenv("LLM_MODEL_NAME")
|
||||||
|
// Ensure service-specific vars are not set
|
||||||
|
os.Unsetenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_BASE_URL")
|
||||||
|
os.Unsetenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_API_KEY")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set default environment variables
|
||||||
|
os.Setenv("OPENAI_BASE_URL", "https://default-base-url.com")
|
||||||
|
os.Setenv("OPENAI_API_KEY", "default-api-key")
|
||||||
|
os.Setenv("LLM_MODEL_NAME", "default-model-name")
|
||||||
|
|
||||||
|
baseURL, apiKey, modelName, err := getModelConfigFromEnv(option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://default-base-url.com", baseURL)
|
||||||
|
assert.Equal(t, "default-api-key", apiKey)
|
||||||
|
assert.Equal(t, "default-model-name", modelName) // Uses default model name when falling back to default config
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetModelConfigFromEnv_MixedConfig(t *testing.T) {
|
||||||
|
// Clean up environment variables after test
|
||||||
|
defer func() {
|
||||||
|
os.Unsetenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_BASE_URL")
|
||||||
|
os.Unsetenv("OPENAI_API_KEY")
|
||||||
|
os.Unsetenv("LLM_MODEL_NAME")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set mixed configuration: service-specific base URL, default API key
|
||||||
|
os.Setenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_BASE_URL", "https://service-specific-url.com")
|
||||||
|
os.Setenv("OPENAI_API_KEY", "default-api-key")
|
||||||
|
os.Setenv("LLM_MODEL_NAME", "default-model-name")
|
||||||
|
|
||||||
|
baseURL, apiKey, modelName, err := getModelConfigFromEnv(option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://service-specific-url.com", baseURL) // Service-specific
|
||||||
|
assert.Equal(t, "default-api-key", apiKey) // Default fallback
|
||||||
|
assert.Equal(t, "doubao-1.5-thinking-vision-pro-250428", modelName) // Service type derived model name
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetModelConfigFromEnv_MissingConfig(t *testing.T) {
|
||||||
|
// Clean up environment variables after test
|
||||||
|
defer func() {
|
||||||
|
os.Unsetenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_BASE_URL")
|
||||||
|
os.Unsetenv("DOUBAO_1_5_THINKING_VISION_PRO_250428_API_KEY")
|
||||||
|
os.Unsetenv("OPENAI_BASE_URL")
|
||||||
|
os.Unsetenv("OPENAI_API_KEY")
|
||||||
|
os.Unsetenv("LLM_MODEL_NAME")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Test missing base URL
|
||||||
|
os.Setenv("OPENAI_API_KEY", "test-api-key")
|
||||||
|
|
||||||
|
_, _, _, err := getModelConfigFromEnv(option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "BASE_URL")
|
||||||
|
|
||||||
|
// Test missing API key
|
||||||
|
os.Unsetenv("OPENAI_API_KEY")
|
||||||
|
os.Setenv("OPENAI_BASE_URL", "https://test-url.com")
|
||||||
|
|
||||||
|
_, _, _, err = getModelConfigFromEnv(option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "API_KEY")
|
||||||
|
|
||||||
|
// Test with both base URL and API key present - should succeed
|
||||||
|
os.Setenv("OPENAI_API_KEY", "test-api-key")
|
||||||
|
|
||||||
|
baseURL, apiKey, modelName, err := getModelConfigFromEnv(option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://test-url.com", baseURL)
|
||||||
|
assert.Equal(t, "test-api-key", apiKey)
|
||||||
|
assert.Equal(t, "doubao-1.5-thinking-vision-pro-250428", modelName) // Model name derived from service type
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaskAPIKey(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
apiKey string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal key",
|
||||||
|
apiKey: "sk-1234567890abcdef",
|
||||||
|
expected: "sk-1******cdef",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "short key",
|
||||||
|
apiKey: "short",
|
||||||
|
expected: "******",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty key",
|
||||||
|
apiKey: "",
|
||||||
|
expected: "******",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exactly 8 chars",
|
||||||
|
apiKey: "12345678",
|
||||||
|
expected: "******",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := maskAPIKey(tt.apiKey)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-3
@@ -89,7 +89,6 @@ func (p *Planner) Call(ctx context.Context, opts *PlanningOptions) (*PlanningRes
|
|||||||
// reset conversation history if requested
|
// reset conversation history if requested
|
||||||
if opts.ResetHistory {
|
if opts.ResetHistory {
|
||||||
p.history.Clear() // Clear everything including system message for complete isolation
|
p.history.Clear() // Clear everything including system message for complete isolation
|
||||||
log.Info().Msg("conversation history reset for planner")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare prompt
|
// prepare prompt
|
||||||
@@ -109,8 +108,8 @@ func (p *Planner) Call(ctx context.Context, opts *PlanningOptions) (*PlanningRes
|
|||||||
logRequest(p.history)
|
logRequest(p.history)
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
message, err := p.model.Generate(ctx, p.history)
|
message, err := p.model.Generate(ctx, p.history)
|
||||||
log.Info().Float64("elapsed(s)", time.Since(startTime).Seconds()).
|
log.Debug().Float64("elapsed(s)", time.Since(startTime).Seconds()).
|
||||||
Str("model", string(p.modelConfig.ModelType)).Msg("call model service")
|
Str("model", string(p.modelConfig.ModelType)).Msg("call model service for planning")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(code.LLMRequestServiceError, err.Error())
|
return nil, errors.Wrap(code.LLMRequestServiceError, err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ func (h *ConversationHistory) Clear() {
|
|||||||
|
|
||||||
// Clear everything including system message
|
// Clear everything including system message
|
||||||
*h = ConversationHistory{}
|
*h = ConversationHistory{}
|
||||||
log.Info().Msg("conversation history cleared completely")
|
log.Warn().Msg("conversation history cleared completely")
|
||||||
}
|
}
|
||||||
|
|
||||||
func logRequest(messages ConversationHistory) {
|
func logRequest(messages ConversationHistory) {
|
||||||
|
|||||||
+2
-2
@@ -261,9 +261,9 @@ func findCachedDriver(platform string) *XTDriver {
|
|||||||
cached.RefCount++
|
cached.RefCount++
|
||||||
|
|
||||||
if platform != "" {
|
if platform != "" {
|
||||||
log.Info().Str("platform", platform).Str("serial", serial).Msg("Using cached XTDriver by platform")
|
log.Debug().Str("platform", platform).Str("serial", serial).Msg("Using cached XTDriver by platform")
|
||||||
} else {
|
} else {
|
||||||
log.Info().Str("serial", serial).Msg("Using any available cached XTDriver")
|
log.Debug().Str("serial", serial).Msg("Using any available cached XTDriver")
|
||||||
}
|
}
|
||||||
return false // stop iteration
|
return false // stop iteration
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user