Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions agent/app/provider/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,21 @@ type Meta struct {
var catalog = map[string]Meta{
"custom": {
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images"),
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
Comment thread
zhengkunwang223 marked this conversation as resolved.
},
"ollama": {
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions"),
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
},
"vllm": {
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images"),
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
},
"deepseek": {
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
APIConfigs: []APIConfig{
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"},
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
},
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
Expand Down Expand Up @@ -131,6 +132,10 @@ var catalog = map[string]Meta{
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"},
{APIType: "openai-embeddings", BaseURL: "https://api.openai.com/v1", Models: []Model{
{ID: "text-embedding-3-small", Name: "text-embedding-3-small"},
{ID: "text-embedding-3-large", Name: "text-embedding-3-large"},
}},
},
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
},
Expand Down Expand Up @@ -296,7 +301,7 @@ func DefaultModels(key, apiType string) []Model {
if len(config.Models) > 0 {
return append([]Model(nil), config.Models...)
}
if IsImageAPIType(config.APIType) {
if IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType) {
return nil
}
break
Expand Down Expand Up @@ -359,7 +364,7 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return "", fmt.Errorf("invalid base url")
}
if key == "custom" && IsImageAPIType(config.APIType) {
if key == "custom" && (IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType)) {
return baseURL, nil
}
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
Expand All @@ -381,6 +386,8 @@ func normalizeEndpointPath(apiType, value string) string {
suffixes = []string{"/responses"}
case "anthropic-messages":
suffixes = []string{"/v1/messages", "/messages"}
case "openai-embeddings":
suffixes = []string{"/v1/embeddings", "/embeddings"}
}
for _, suffix := range suffixes {
if strings.HasSuffix(strings.ToLower(path), suffix) {
Expand All @@ -390,6 +397,10 @@ func normalizeEndpointPath(apiType, value string) string {
return path
}

func IsEmbeddingAPIType(apiType string) bool {
return apiType == "openai-embeddings"
}

func IsImageAPIType(apiType string) bool {
switch apiType {
case "openai-images", "dashscope-images", "minimax-images", "openrouter-images":
Expand Down
2 changes: 1 addition & 1 deletion agent/app/provider/openclaw.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
resolvedAPIType = DefaultAPIType(provider)
}
if IsImageAPIType(resolvedAPIType) {
if IsImageAPIType(resolvedAPIType) || IsEmbeddingAPIType(resolvedAPIType) {
return nil, fmt.Errorf("api type %s does not support text generation", resolvedAPIType)
}
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
Expand Down
15 changes: 15 additions & 0 deletions agent/app/provider/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
}

switch apiType {
case "openai-embeddings":
request.URL = embeddingVerifyURL(baseURL)
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "ping"})
case "openai-images":
request.URL = imageVerifyURL(provider, baseURL, "/images/generations")
headers["Authorization"] = "Bearer " + apiKey
Expand Down Expand Up @@ -131,6 +135,17 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
return request
}

func embeddingVerifyURL(baseURL string) string {
lowerBaseURL := strings.ToLower(baseURL)
if strings.HasSuffix(lowerBaseURL, "/embeddings") {
return baseURL
}
if strings.HasSuffix(lowerBaseURL, "/v1") {
return baseURL + "/embeddings"
}
return baseURL + "/v1/embeddings"
}

func imageVerifyURL(provider, baseURL, endpoint string) string {
if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) {
return baseURL
Expand Down
2 changes: 1 addition & 1 deletion agent/app/repo/agent_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
Model(&model.AgentAccount{}).
Select("provider, COUNT(*) as count").
Where("provider IN ?", normalizedProviders).
Where("api_type NOT LIKE ?", "%-images").
Scopes(WithTextAPIType()).
Group("provider").
Scan(&rows).Error; err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion agent/app/repo/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func WithByAPIType(apiType string) DBOption {

func WithTextAPIType() DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("api_type NOT LIKE ?", "%-images")
return g.Where("api_type NOT LIKE ? AND api_type <> ?", "%-images", "openai-embeddings")
}
}

Expand Down
2 changes: 1 addition & 1 deletion agent/app/service/agents_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
}
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
if validateAvailability && (imageAPI || !providercatalog.SkipVerification(provider)) {
if validateAvailability && (imageAPI || providercatalog.IsEmbeddingAPIType(resolvedAPIType) || !providercatalog.SkipVerification(provider)) {
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
return resolvedAgentAccountInput{}, err
}
Expand Down
8 changes: 4 additions & 4 deletions agent/app/service/app_upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {

logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
t.LogStart(logStr)
if out, upErr := compose.UpWithoutPull(u.original.GetComposePath()); upErr != nil {
if out, upErr := compose.UpWithoutBuild(u.original.GetComposePath()); upErr != nil {
if out != "" {
upErr = fmt.Errorf("%s: %w", out, upErr)
}
Expand Down Expand Up @@ -545,7 +545,7 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
return u.finishRollback()
}
if u.phase < appUpgradeMutated {
if out, err := compose.UpWithoutPull(u.original.GetComposePath()); err != nil {
if out, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
Expand All @@ -563,14 +563,14 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
if u.backupFile != "" {
_ = u.restoreManagedFiles()
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil {
_, _ = compose.UpWithoutPull(u.original.GetComposePath())
_, _ = compose.UpWithoutBuild(u.original.GetComposePath())
return errors.Join(rollbackErr, err)
}
} else {
if err := u.restoreManagedFiles(); err != nil {
return errors.Join(rollbackErr, err)
}
if out, err := compose.UpWithoutPull(u.original.GetComposePath()); err != nil {
if out, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
Expand Down
2 changes: 1 addition & 1 deletion agent/app/service/backup_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
}
defer func() {
if isRollback {
_, _ = compose.UpWithoutPull(install.GetComposePath())
_, _ = compose.UpWithoutBuild(install.GetComposePath())
} else {
_, _ = compose.Up(install.GetComposePath())
}
Expand Down
8 changes: 4 additions & 4 deletions agent/utils/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func Up(filePath string, projectName ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
}

func UpWithoutPull(filePath string, projectName ...string) (string, error) {
func UpWithoutBuild(filePath string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil {
return "", err
}
Expand All @@ -58,11 +58,11 @@ func UpWithoutPull(filePath string, projectName ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
}

func upArgs(filePath string, withoutPull bool) []string {
func upArgs(filePath string, withoutBuild bool) []string {
args := loadFiles(filePath)
args = append(args, "up", "-d")
if withoutPull {
args = append(args, "--pull", "never", "--no-build")
if withoutBuild {
args = append(args, "--no-build")
}
return args
}
Expand Down
46 changes: 10 additions & 36 deletions agent/utils/docker/compose.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package docker

import (
"bufio"
"bytes"
"context"
"fmt"
"path"
"strings"

"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -91,7 +92,7 @@ func (e *Environment) UnmarshalYAML(value *yaml.Node) error {
}

func GetImagesFromDockerCompose(env, yml []byte) ([]string, error) {
envVars, err := loadEnvFile(env)
envVars, err := dotenv.Parse(bytes.NewReader(env))
if err != nil {
return nil, fmt.Errorf("load env failed: %v", err)
}
Expand All @@ -104,43 +105,16 @@ func GetImagesFromDockerCompose(env, yml []byte) ([]string, error) {
var images []string
for _, service := range compose.Services {
if service.Image != "" {
resolvedImage := replaceEnvVars(service.Image, envVars)
resolvedImage, err := template.Substitute(service.Image, func(key string) (string, bool) {
value, ok := envVars[key]
return value, ok
})
if err != nil {
return nil, fmt.Errorf("resolve image failed: %v", err)
}
images = append(images, resolvedImage)
}
}

return images, nil
}

func loadEnvFile(env []byte) (map[string]string, error) {
envVars := make(map[string]string)

scanner := bufio.NewScanner(bytes.NewReader(env))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())

if line == "" || strings.HasPrefix(line, "#") {
continue
}

parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
value = strings.Trim(value, `"'`)
envVars[key] = value
}
}

return envVars, scanner.Err()
}

func replaceEnvVars(input string, envVars map[string]string) string {
return re.GetRegex(re.ComposeEnvVarPattern).ReplaceAllStringFunc(input, func(match string) string {
varName := match[2 : len(match)-1]
if value, exists := envVars[varName]; exists {
return value
}
return match
})
}
19 changes: 14 additions & 5 deletions frontend/src/components/api-type-tag/index.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<template>
<el-tag
size="small"
effect="dark"
effect="light"
:type="tagType"
:class="{ 'api-type-tag--responses': apiType === 'openai-responses' }"
:class="{
'api-type-tag--responses': apiType === 'openai-responses',
'api-type-tag--embedding': apiType === 'openai-embeddings',
}"
>
{{ apiType || '-' }}
</el-tag>
Expand Down Expand Up @@ -37,8 +40,14 @@ const tagType = computed<TagType>(() => {

<style scoped lang="scss">
.api-type-tag--responses {
--el-tag-bg-color: #7c3aed;
--el-tag-border-color: #7c3aed;
--el-tag-text-color: #ffffff;
--el-tag-bg-color: #f3e8ff;
--el-tag-border-color: #d8b4fe;
--el-tag-text-color: #7e22ce;
}

.api-type-tag--embedding {
--el-tag-bg-color: #e6f4f1;
--el-tag-border-color: #a7d9d1;
--el-tag-text-color: #0f766e;
}
</style>
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1159,8 +1159,10 @@ const message = {
imageGeneration: 'Image Generation',
textModels: 'Text Models',
textToImageModels: 'Text-to-Image Models',
embeddingModels: 'Embedding Models',
unauthorized: 'Unauthorized',
imageCount: 'Image Count',
requestExample: 'Request Example',
availableModels: 'Available Models',
modelGroupModels: 'Request Models',
modelGroupModelsPlaceholder: 'Select or enter request model names',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/es-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1165,8 +1165,10 @@ const message = {
imageGeneration: 'Generación de imágenes',
textModels: 'Modelos de texto',
textToImageModels: 'Modelos de texto a imagen',
embeddingModels: 'Modelos de embeddings',
unauthorized: 'Sin autorización',
imageCount: 'Cantidad de imágenes',
requestExample: 'Ejemplo de solicitud',
availableModels: 'Modelos disponibles',
modelGroupModels: 'Modelos solicitados',
modelGroupModelsPlaceholder: 'Seleccione o ingrese nombres de modelos solicitados',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1145,8 +1145,10 @@ const message = {
imageGeneration: 'تولید تصویر',
textModels: 'مدل‌های متنی',
textToImageModels: 'مدل‌های متن به تصویر',
embeddingModels: 'مدل‌های امبدینگ',
unauthorized: 'بدون مجوز',
imageCount: 'تعداد تصاویر',
requestExample: 'نمونه درخواست',
availableModels: 'مدل‌های موجود',
modelGroupModels: 'مدل‌های درخواست',
modelGroupModelsPlaceholder: 'نام‌های مدل درخواست را انتخاب یا وارد کنید',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,8 +1148,10 @@ const message = {
imageGeneration: '画像生成',
textModels: 'テキストモデル',
textToImageModels: 'テキスト画像生成モデル',
embeddingModels: 'Embeddingモデル',
unauthorized: '未承認',
imageCount: '画像数',
requestExample: 'リクエスト例',
availableModels: '利用可能なモデル',
modelGroupModels: 'リクエストモデル',
modelGroupModelsPlaceholder: 'リクエストモデル名を選択または入力してください',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1137,8 +1137,10 @@ const message = {
imageGeneration: '이미지 생성',
textModels: '텍스트 모델',
textToImageModels: '텍스트-이미지 모델',
embeddingModels: '임베딩 모델',
unauthorized: '권한 없음',
imageCount: '이미지 수',
requestExample: '요청 예시',
availableModels: '사용 가능한 모델',
modelGroupModels: '요청 모델',
modelGroupModelsPlaceholder: '요청 모델 이름을 선택하거나 입력하세요',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/lo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1138,8 +1138,10 @@ const message = {
imageGeneration: 'ການສ້າງຮູບພາບ',
textModels: 'ໂມເດວຂໍ້ຄວາມ',
textToImageModels: 'ໂມເດວສ້າງຮູບຈາກຂໍ້ຄວາມ',
embeddingModels: 'ໂມເດວ Embedding',
unauthorized: 'ບໍ່ມີສິດ',
imageCount: 'ຈຳນວນຮູບ',
requestExample: 'ຕົວຢ່າງຄຳຂໍ',
availableModels: 'ໂມເດວທີ່ມີໃຫ້ໃຊ້',
modelGroupModels: 'ໂມເດວທີ່ຮ້ອງຂໍ',
modelGroupModelsPlaceholder: 'ເລືອກ ຫຼື ປ້ອນຊື່ໂມເດວທີ່ຮ້ອງຂໍ',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lang/modules/ms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1170,8 +1170,10 @@ const message = {
imageGeneration: 'Penjanaan Imej',
textModels: 'Model Teks',
textToImageModels: 'Model Teks-ke-Imej',
embeddingModels: 'Model Embedding',
unauthorized: 'Tidak dibenarkan',
imageCount: 'Bilangan Imej',
requestExample: 'Contoh Permintaan',
availableModels: 'Model Tersedia',
modelGroupModels: 'Model Permintaan',
modelGroupModelsPlaceholder: 'Pilih atau masukkan nama model permintaan',
Expand Down
Loading
Loading