OpenWebUI优化: models 图像批量替换成 CDN 地址脚本分享

前言

OpenWebUI 在模型管理上有一个非常不合理的设计:所有模型的 logo 图标都以 base64 格式直接嵌入在配置文件中,导致 /v1/models/api/models 请求体积巨大。base64 编码不仅让单个 logo 体积增大约 30%,而且所有模型的 logo 都会被一次性拉取,前端首次加载极其卡慢,尤其是在带宽有限的 VPS 或小鸡上,体验极差。 :sweat_smile:

更离谱的是,这些 base64 图标因为被放在 API 路径下,无法享受 CDN 缓存优化,导致每次刷新都要重新拉取全部模型信息和 logo,进一步加剧了带宽和加载压力。

参照:OpenwebUI 优化:models 庞大体积优化及CDN缓存配置。

解决思路

  • 将所有模型的 profile_image_url 字段批量替换为外链 SVG 图标(如 lobehub 提供的 CDN 地址),极大减小配置文件体积。
  • 通过 CDN 缓存这些静态资源,前端只需加载一次 logo,后续访问均可走缓存,极大提升加载速度和带宽利用率。
  • 这样不仅优化了 OpenWebUI 的加载体验,也为后续自定义和维护模型图标提供了极大便利。

让 Cline 帮我写了个 python 脚本用于批量替换

1. 在 OpenWebUI 中导出模型预设

  • 打开 OpenWebUI,进入 “模型” 管理页面。
  • 需要首先对模型有所修改,比如增加描述,导出的 metadata 才会有 profile_image_url 参数
  • 在页面右下角,点击 “导出预设” 按钮(如下图红色箭头所示),将当前所有模型配置导出为本地 JSON 文件。

2. 使用脚本批量修正 profile_image_url

  • 自动查找最新导出的 JSON 文件并覆盖原文件:
python update_profile_image_url.py
  • 指定输入文件并输出到新文件:
python update_profile_image_url.py models-export-xxxxxx.json -o output.json
  • 仅预览详细日志不写入文件(推荐先 dry-run 检查):
python update_profile_image_url.py --dry-run

3. 在 OpenWebUI 中导入修正后的模型预设

  • 回到 “模型” 管理页面,点击 “导入预设” 按钮,选择刚刚修正过的 JSON 文件导入即可。

脚本如下:

"""
name: 批量修正 OpenWebUI 导出的模型 JSON 文件中 profile_image_url 字段的脚本
author: Hardship2495 & Cline 
version: 1.0

【功能说明】
- 根据模型 id 中的关键词,自动匹配并修正每个模型的 profile_image_url 字段。
- 只在 profile_image_url 为空、为默认值或错误时才进行替换,已是正确 URL 则跳过。
- 匹配优先级:提供商前缀 > 关键词,顺序可在 PROVIDER_MAP 中维护。
- 支持详细日志输出,显示每条记录的处理情况。

【使用方法】
1. 自动查找当前目录下最新的 models-export-*.json 文件并覆盖原文件:
   python update_profile_image_url.py

2. 指定输入文件并输出到新文件:
   python update_profile_image_url.py models-export-xxxxxxx.json -o output.json

3. 仅预览详细日志不写入文件(推荐先 dry-run 检查):
   python update_profile_image_url.py --dry-run

【参数说明】
- json_file      输入的模型配置 JSON 文件路径(可选,默认自动查找最新)
- -o, --output   输出文件名(可选,默认覆盖原文件)
- --dry-run      只预览详细日志,不写入文件

【映射表维护】
- PROVIDER_MAP 为 (关键词,image_url) 的有序列表,支持随时增删和调整优先级。
- 关键词区分优先级,前缀(如 openrouter/)优先于普通关键词(如 gpt)。
- 若有新模型或新提供商,只需在 PROVIDER_MAP 中添加对应项即可。

【日志说明】
- [UPDATE]   表示已替换的模型,显示 id、原始 URL、新 URL
- [SKIP]     表示无需替换的模型,显示 id 及原因
- [NOT FOUND] 未匹配到任何关键词的模型,显示 id 及当前 URL

【适用场景】
- OpenWebUI 导出的模型配置批量修正
- 需要统一或纠正 profile_image_url 字段的场景

"""

import os
import sys
import json
import glob
import argparse

# 1. 维护模型提供商关键词与 image_url 的映射(可随时增删)
PROVIDER_MAP = [
    # (关键词,image_url),顺序即优先级
    ("openrouter/", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openrouter.svg"),
    ("aliyun/", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/aliyun-color.svg"),
    ("gemini_pipe_new.", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg"),
    ("gpt", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"),
    ("openai", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"),
    ("o1-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"),
    ("o3-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"),
    ("whisper", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"),
    ("text-embedding-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg"),
    ("tts-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg"),
    ("dall-e", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle-color.svg"),
    ("claude", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/claude-color.svg"),
    ("gemini", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg"),
    ("ernie", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin-color.svg"),
    ("baidu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin-color.svg"),
    ("command", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere-color.svg"),
    ("cohere", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere-color.svg"),
    ("deepseek", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek-color.svg"),
    ("grok", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/grok.svg"),
    ("llama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta-color.svg"),
    ("meta", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta-color.svg"),
    ("groq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/groq-color.svg"),
    ("qwq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg"),
    ("qvq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg"),
    ("qwen", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen-color.svg"),
    ("abab", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax-color.svg"),
    ("minimax", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax-color.svg"),
    ("mistral", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral-color.svg"),
    ("kimi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi-color.svg"),
    ("moonshot", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi-color.svg"),
    ("ollama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama-color.svg"),
    ("hunyuan", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan-color.svg"),
    ("tencent", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan-color.svg"),
    ("yi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/yi-color.svg"),
    ("glm", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan-color.svg"),
    ("zhipu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan-color.svg"),
]

DEFAULT_URLS = {"","/static/favicon.png"}

def find_correct_url (model_id: str) -> str:
    """根据 id 匹配对应的 image_url,优先顺序为 PROVIDER_MAP 顺序"""
    id_lower = model_id.lower ()
    for key, url in PROVIDER_MAP:
        if key in id_lower:
            return url
    return None

def get_latest_json_file ():
    files = glob.glob ("models-export-*.json")
    if not files:
        print ("未找到 models-export-*.json 文件。")
        sys.exit (1)
    files.sort (key=os.path.getmtime, reverse=True)
    return files [0]

def main ():
    parser = argparse.ArgumentParser (description="批量修正 OpenWebUI 模型 profile_image_url")
    parser.add_argument ("json_file", nargs="?", help="模型配置 JSON 文件路径(可选,默认自动查找最新)")
    parser.add_argument ("-o", "--output", help="输出文件名(可选,默认覆盖原文件)")
    parser.add_argument ("--dry-run", action="store_true", help="只预览不写入文件")
    args = parser.parse_args ()

    json_file = args.json_file or get_latest_json_file ()
    with open (json_file, "r", encoding="utf-8") as f:
        data = json.load (f)

    updated, skipped, not_found = 0, 0, 0
    update_logs = []
    skip_logs = []
    notfound_logs = []

    for model in data:
        model_id = model.get ("id", "")
        meta = model.get ("meta", {})
        if not meta:
            continue
        current_url = meta.get ("profile_image_url", "")
        correct_url = find_correct_url (model_id)
        if correct_url:
            if current_url == correct_url:
                skipped += 1
                skip_logs.append (f"[SKIP] {model_id} 已是正确 URL: {current_url}")
            elif current_url in DEFAULT_URLS or not current_url:
                update_logs.append (f"[UPDATE] {model_id}\n  原 URL: {current_url}\n  新 URL: {correct_url}")
                meta ["profile_image_url"] = correct_url
                updated += 1
            else:
                skipped += 1
                skip_logs.append (f"[SKIP] {model_id} URL 已存在且与映射不符: {current_url}")
        else:
            not_found += 1
            notfound_logs.append (f"[NOT FOUND] {model_id} 未匹配到任何提供商关键词,当前 URL: {current_url}")

    print (f"处理完成:共 {len (data)} 条,更新 {updated} 条,跳过 {skipped} 条,未匹配到提供商 {not_found} 条。")
    print ("="*40)
    if update_logs:
        print ("更新记录:")
        for log in update_logs:
            print (log)
    if skip_logs:
        print ("\n 跳过记录:")
        for log in skip_logs:
            print (log)
    if notfound_logs:
        print ("\n 未匹配到提供商的模型:")
        for log in notfound_logs:
            print (log)
    print ("="*40)

    if not args.dry_run:
        output_file = args.output or json_file
        with open (output_file, "w", encoding="utf-8") as f:
            json.dump (data, f, ensure_ascii=False, indent=2)
        print (f"已写入:{output_file}")
    else:
        print ("dry-run 预览模式,未写入文件。")

if __name__ == "__main__":
    main ()

脚本运行结果

再次感谢 lobechat 提供的图像 CDN 服务 :saluting_face:

欢迎佬友们试用和反馈建议! :brown_heart:

42 Likes

试了,非常好用

2 Likes

所有模型的 logo 图标都以 base64 格式直接嵌入在配置文件中
这操作简直离谱,官方没人提issue吗

1 Like

佬 简单看了一下 发现有些模型没有profile_image_url这个参数 不会自动添加上去嘛

2 Likes

原文写了:

  • 需要首先对模型有所修改,比如增加描述,导出的 metadata 才会有 profile_image_url 参数
2 Likes

用上了, 谢谢佬分享

2 Likes

感谢佬友分享,这下修改起来方便多啦 :smiling_face_with_three_hearts:

2 Likes

感谢大佬!

2 Likes

openwebui就是草台班子写的,难用,迟钝

2 Likes

将佬友的改成网页了(gemini-2.5-pro 改的,一次成型)

佬友做的确实比我那版好 :tieba_087:

11 Likes

效果拔群,實用性拉滿

1 Like

刚刚部署open webui感谢大佬

感谢大佬

謝謝佬,回家弄弄

这个直接上传json文件,然后处理模型文件就可以了吗

对的,和佬友的脚本一样的功能与实现

感谢大佬,又有折腾了

多谢多谢,这个方便

构思东西,我串了一辈子都想不出把静态资源嵌在动态api请求这种模式

感谢佬的分享

这个参数不能自己插吗