mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-10 17:43:00 +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
|
||||
|
||||
@@ -103,8 +103,8 @@ type ModelConfig struct {
|
||||
- 多模型类型支持
|
||||
|
||||
**支持的模型类型**:
|
||||
- `LLMServiceTypeUITARS`: UI-TARS 专业 UI 自动化模型
|
||||
- `LLMServiceTypeDoubaoVL`: 豆包视觉语言模型
|
||||
- `DOUBAO_1_5_THINKING_VISION_PRO_250428`: 豆包思维视觉专业版
|
||||
- `DOUBAO_1_5_UI_TARS_250428`: 豆包UI-TARS专业UI自动化模型
|
||||
|
||||
### 2. 智能规划器 (planner.go)
|
||||
|
||||
@@ -290,30 +290,120 @@ type ConversationHistory []*schema.Message
|
||||
|
||||
### 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
|
||||
export OPENAI_BASE_URL="https://your-api-endpoint"
|
||||
export OPENAI_API_KEY="your-api-key"
|
||||
export LLM_MODEL_NAME="your-model-name"
|
||||
# 默认配置
|
||||
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
|
||||
|
||||
# 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 服务
|
||||
|
||||
#### 在测试用例中指定服务
|
||||
|
||||
```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
|
||||
// 创建 UI-TARS 服务
|
||||
llmService, err := ai.NewLLMService(option.LLMServiceTypeUITARS)
|
||||
// 创建豆包思维视觉专业版服务
|
||||
llmService, err := ai.NewLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to create LLM service")
|
||||
}
|
||||
|
||||
// 创建豆包视觉服务
|
||||
llmService, err := ai.NewLLMService(option.LLMServiceTypeDoubaoVL)
|
||||
// 创建豆包UI-TARS服务
|
||||
llmService, err := ai.NewLLMService(option.DOUBAO_1_5_UI_TARS_250428)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to create LLM service")
|
||||
}
|
||||
```
|
||||
|
||||
#### 模型切换
|
||||
|
||||
要切换到不同的模型服务,只需要修改测试用例中的 `llm_service` 字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"name": "连连看游戏测试",
|
||||
"llm_service": "doubao-1.5-ui-tars-250428"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
系统会自动根据服务名称获取对应的配置,无需修改环境变量。
|
||||
|
||||
### 3. 智能规划使用
|
||||
|
||||
```go
|
||||
@@ -446,8 +536,8 @@ log.Info().Float64("x", center.X).Float64("y", center.Y).
|
||||
|
||||
AI 模块支持多种不同的语言模型,每种模型都有其特定的优势:
|
||||
|
||||
- **UI-TARS**: 专门针对 UI 自动化优化的模型,支持 Thought/Action 格式
|
||||
- **豆包视觉**: 通用视觉语言模型,支持结构化 JSON 输出
|
||||
- **豆包思维视觉专业版**: 支持深度思考的视觉语言模型,适合复杂场景分析
|
||||
- **豆包UI-TARS**: 专门针对 UI 自动化优化的模型,支持 Thought/Action 格式
|
||||
|
||||
### 2. 坐标系统转换
|
||||
|
||||
@@ -495,7 +585,8 @@ func normalizeParameterName(paramName string) string {
|
||||
### 1. 环境变量配置
|
||||
- 确保所有必需的环境变量都已正确设置
|
||||
- API 密钥需要有足够的权限和配额
|
||||
- 模型名称必须与服务类型匹配
|
||||
- 支持多模型配置,可以同时配置多个服务
|
||||
- 模型名称自动从服务类型推导,无需手动配置
|
||||
|
||||
### 2. 图像格式要求
|
||||
- 支持 Base64 编码的图像数据
|
||||
@@ -503,7 +594,7 @@ func normalizeParameterName(paramName string) string {
|
||||
- 图像尺寸信息必须准确提供
|
||||
|
||||
### 3. 坐标系统
|
||||
- UI-TARS 使用 1000x1000 相对坐标系统
|
||||
- 豆包UI-TARS 使用 1000x1000 相对坐标系统
|
||||
- 需要正确的屏幕尺寸信息进行坐标转换
|
||||
- 注意不同模型的坐标格式差异
|
||||
|
||||
|
||||
106
uixt/ai/ai.go
106
uixt/ai/ai.go
@@ -2,17 +2,8 @@ package ai
|
||||
|
||||
import (
|
||||
"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/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
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:]
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ Here is the assertion. Please tell whether it is truthy according to the screens
|
||||
logRequest(a.history)
|
||||
startTime := time.Now()
|
||||
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")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.LLMRequestServiceError, err.Error())
|
||||
|
||||
130
uixt/ai/env.go
Normal file
130
uixt/ai/env.go
Normal file
@@ -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:]
|
||||
}
|
||||
171
uixt/ai/env_test.go
Normal file
171
uixt/ai/env_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,6 @@ func (p *Planner) Call(ctx context.Context, opts *PlanningOptions) (*PlanningRes
|
||||
// reset conversation history if requested
|
||||
if opts.ResetHistory {
|
||||
p.history.Clear() // Clear everything including system message for complete isolation
|
||||
log.Info().Msg("conversation history reset for planner")
|
||||
}
|
||||
|
||||
// prepare prompt
|
||||
@@ -109,8 +108,8 @@ func (p *Planner) Call(ctx context.Context, opts *PlanningOptions) (*PlanningRes
|
||||
logRequest(p.history)
|
||||
startTime := time.Now()
|
||||
message, err := p.model.Generate(ctx, p.history)
|
||||
log.Info().Float64("elapsed(s)", time.Since(startTime).Seconds()).
|
||||
Str("model", string(p.modelConfig.ModelType)).Msg("call model service")
|
||||
log.Debug().Float64("elapsed(s)", time.Since(startTime).Seconds()).
|
||||
Str("model", string(p.modelConfig.ModelType)).Msg("call model service for planning")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(code.LLMRequestServiceError, err.Error())
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (h *ConversationHistory) Clear() {
|
||||
|
||||
// Clear everything including system message
|
||||
*h = ConversationHistory{}
|
||||
log.Info().Msg("conversation history cleared completely")
|
||||
log.Warn().Msg("conversation history cleared completely")
|
||||
}
|
||||
|
||||
func logRequest(messages ConversationHistory) {
|
||||
|
||||
@@ -261,9 +261,9 @@ func findCachedDriver(platform string) *XTDriver {
|
||||
cached.RefCount++
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user