feat(builtin-models): add YAML-based declarative config with ${ENV} interpolation
Allow built-in models to be declared in config/builtin_models.yaml
instead of inserting rows via SQL. On every startup the file is read
and each entry is UPSERT-ed into the models table (is_builtin=true)
by stable id.
Any string field may reference an environment variable with ${NAME}.
Unset variables are left as the literal placeholder so
misconfiguration surfaces clearly in provider calls rather than
failing silently with an empty token.
The file is optional: missing file, parse errors, and per-entry
upsert failures all log a warning without aborting startup.
docker-compose.yml adds env_file (.env, required:false) so
deployment-specific variables are passed through automatically.
This commit is contained in:
@@ -18,6 +18,27 @@ GIN_MODE=release
|
||||
# 可选值:true(自动放在 LOG_PATH 同目录下 llm_debug.log)、false/空(关闭)、或指定文件路径
|
||||
# LLM_DEBUG_LOG=true
|
||||
|
||||
# ========== 内置模型(Built-in Models)声明式配置(可选) ==========
|
||||
# 如果启用了 config/builtin_models.yaml(见 config/builtin_models.yaml.example),
|
||||
# YAML 中的 ${NAME} 占位符会在应用启动期从这里读取真实值。变量名由 YAML 自行
|
||||
# 指定,下面给出常见命名作为参考;按你自己的部署改名也可以。详见
|
||||
# docs/BUILTIN_MODELS.md。
|
||||
#
|
||||
# LLM_MODEL_NAME=
|
||||
# LLM_BASE_URL=
|
||||
# LLM_API_KEY=
|
||||
# LLM_PROVIDER=openai
|
||||
#
|
||||
# EMBEDDING_MODEL_NAME=
|
||||
# EMBEDDING_BASE_URL=
|
||||
# EMBEDDING_API_KEY=
|
||||
# EMBEDDING_PROVIDER=openai
|
||||
#
|
||||
# RERANK_MODEL_NAME=
|
||||
# RERANK_BASE_URL=
|
||||
# RERANK_API_KEY=
|
||||
# RERANK_PROVIDER=generic
|
||||
|
||||
# ========== Langfuse 可观测性(可选) ==========
|
||||
# 用于追踪 chat / embedding / rerank / VLM / ASR 模型调用,统计 token 消耗。
|
||||
# 详细说明:docs/Langfuse集成.md
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# config/builtin_models.yaml — declarative built-in models.
|
||||
#
|
||||
# Copy to config/builtin_models.yaml (or point BUILTIN_MODELS_CONFIG at a custom
|
||||
# path) to enable. Each entry below is inserted into the `models` table with
|
||||
# is_builtin=true and becomes visible to every tenant.
|
||||
#
|
||||
# How values reach the file
|
||||
# -------------------------
|
||||
# Any string field can reference an env var with ${NAME}. Unset env vars are
|
||||
# left as the literal ${NAME} so misconfiguration surfaces clearly in upstream
|
||||
# API calls instead of producing a silent empty token. Non-string fields
|
||||
# (type, source, is_default, dimension, truncate_prompt_tokens) must stay
|
||||
# literal because YAML parses them as their target type.
|
||||
#
|
||||
# How env vars reach the container
|
||||
# --------------------------------
|
||||
# By default the app service in docker-compose.yml loads .env via
|
||||
# env_file:
|
||||
# - path: .env
|
||||
# required: false
|
||||
# so anything you put in .env is auto-passed to the container. There is no
|
||||
# need to list each var separately under `environment:`.
|
||||
#
|
||||
# Re-applied on every application startup — editing the file and restarting is
|
||||
# enough for changes to take effect. Entries removed from the file are NOT
|
||||
# auto-deleted from the database; clean them up manually if you need to.
|
||||
|
||||
builtin_models: []
|
||||
|
||||
# ----- Example: env-driven entries — uncomment and adapt --------------------
|
||||
#
|
||||
# builtin_models:
|
||||
# - id: builtin-llm-default
|
||||
# type: KnowledgeQA # KnowledgeQA | Embedding | Rerank | VLLM | ASR
|
||||
# source: remote # remote (default) | local | ...
|
||||
# is_default: true
|
||||
# name: ${LLM_MODEL_NAME}
|
||||
# parameters:
|
||||
# base_url: ${LLM_BASE_URL}
|
||||
# api_key: ${LLM_API_KEY}
|
||||
# provider: ${LLM_PROVIDER} # openai | generic | aliyun | moonshot | ...
|
||||
#
|
||||
# - id: builtin-embedding-default
|
||||
# type: Embedding
|
||||
# source: remote
|
||||
# is_default: true
|
||||
# name: ${EMBEDDING_MODEL_NAME}
|
||||
# parameters:
|
||||
# base_url: ${EMBEDDING_BASE_URL}
|
||||
# api_key: ${EMBEDDING_API_KEY}
|
||||
# provider: ${EMBEDDING_PROVIDER}
|
||||
# embedding_parameters:
|
||||
# dimension: 1536
|
||||
# truncate_prompt_tokens: 0
|
||||
#
|
||||
# - id: builtin-rerank-default
|
||||
# type: Rerank
|
||||
# source: remote
|
||||
# parameters:
|
||||
# base_url: ${RERANK_BASE_URL}
|
||||
# api_key: ${RERANK_API_KEY}
|
||||
# provider: ${RERANK_PROVIDER}
|
||||
#
|
||||
# ----- Example: literal values (no env indirection) -------------------------
|
||||
#
|
||||
# builtin_models:
|
||||
# - id: builtin-openai-chat
|
||||
# name: gpt-4o-mini
|
||||
# type: KnowledgeQA
|
||||
# source: remote
|
||||
# is_default: false
|
||||
# parameters:
|
||||
# base_url: https://api.openai.com/v1
|
||||
# api_key: ${OPENAI_API_KEY}
|
||||
# provider: openai
|
||||
@@ -41,12 +41,23 @@ services:
|
||||
- ./config/config.yaml:/app/config/config.yaml
|
||||
# Optional: mount custom skills directory (allows adding skills without rebuilding image)
|
||||
- ./skills/preloaded:/app/skills/preloaded
|
||||
# Optional: declarative built-in models. Copy config/builtin_models.yaml.example
|
||||
# to config/builtin_models.yaml, edit it for your deployment, then
|
||||
# uncomment the line below. See docs/BUILTIN_MODELS.md.
|
||||
# - ./config/builtin_models.yaml:/app/config/builtin_models.yaml:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
# Optional: source .env so deployment-specific vars (e.g. those referenced
|
||||
# by config/builtin_models.yaml via ${ENV}) reach the app container without
|
||||
# having to be listed individually under `environment:`. Falls back
|
||||
# silently when .env is absent (e.g. fresh upstream clone).
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
environment:
|
||||
- LOG_LEVEL=${LOG_LEVEL:-}
|
||||
- COS_SECRET_ID=${COS_SECRET_ID:-}
|
||||
|
||||
+187
-105
@@ -13,163 +13,245 @@
|
||||
|
||||
## 如何添加内置模型
|
||||
|
||||
内置模型需要通过数据库直接插入。以下是添加内置模型的步骤:
|
||||
WeKnora 支持两种方式添加内置模型:**推荐**使用 YAML 声明式配置(自动幂等下发),保留 SQL 直插作为兼容路径。
|
||||
|
||||
### 1. 准备模型数据
|
||||
### 方式一(推荐):YAML 配置文件
|
||||
|
||||
首先,确保你已经有了要设置为内置模型的模型配置信息,包括:
|
||||
- 模型名称(name)
|
||||
- 模型类型(type):`KnowledgeQA`、`Embedding`、`Rerank` 或 `VLLM`
|
||||
- 模型来源(source):`local` 或 `remote`
|
||||
- 模型参数(parameters):包括 base_url、api_key、provider 等
|
||||
- 租户ID(tenant_id):建议使用小于10000的租户ID,避免冲突
|
||||
#### 文件位置
|
||||
|
||||
**支持的服务商(provider)**:`generic`(自定义)、`openai`、`aliyun`、`zhipu`、`volcengine`、`hunyuan`、`deepseek`、`minimax`、`mimo`、`siliconflow`、`jina`、`openrouter`、`gemini`、`modelscope`、`moonshot`、`qianfan`、`qiniu`、`longcat`、`gpustack`
|
||||
默认路径是 `config/builtin_models.yaml`(与 `config.yaml`、`builtin_agents.yaml` 同目录)。如需挂载到其他位置,设置环境变量 `BUILTIN_MODELS_CONFIG=/absolute/path/builtin_models.yaml` 覆盖。
|
||||
|
||||
### 2. 执行 SQL 插入语句
|
||||
文件不存在时启动期会跳过、不报错;解析失败仅记录 Warning、不影响主流程。每次应用启动会重新读取并按 `id` 字段 UPSERT 到 `models` 表(保留 `created_at`,刷新其他字段)。
|
||||
|
||||
使用以下 SQL 语句插入内置模型:
|
||||
#### Schema
|
||||
|
||||
```yaml
|
||||
builtin_models:
|
||||
- id: <required, stable, UPSERT key>
|
||||
tenant_id: <int, default 10000> # 与 tenants_id_seq 起点对齐
|
||||
name: <string>
|
||||
type: KnowledgeQA | Embedding | Rerank | VLLM | ASR
|
||||
source: <string, default "remote"> # local | remote | aliyun | ...
|
||||
description: <string, optional>
|
||||
is_default: <bool, default false>
|
||||
status: <string, default "active">
|
||||
parameters:
|
||||
base_url: <string>
|
||||
api_key: <string, supports ${ENV_VAR}>
|
||||
provider: <string> # openai | generic | moonshot | ...
|
||||
embedding_parameters: # 仅 Embedding 类型
|
||||
dimension: <int>
|
||||
truncate_prompt_tokens: <int>
|
||||
```
|
||||
|
||||
#### 完整示例
|
||||
|
||||
```yaml
|
||||
builtin_models:
|
||||
- id: builtin-openai-chat
|
||||
name: gpt-4o-mini
|
||||
type: KnowledgeQA
|
||||
source: remote
|
||||
description: OpenAI 默认对话模型
|
||||
is_default: false
|
||||
parameters:
|
||||
base_url: https://api.openai.com/v1
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
provider: openai
|
||||
|
||||
- id: builtin-openai-embeddings
|
||||
name: text-embedding-3-small
|
||||
type: Embedding
|
||||
source: remote
|
||||
parameters:
|
||||
base_url: https://api.openai.com/v1
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
provider: openai
|
||||
embedding_parameters:
|
||||
dimension: 1536
|
||||
truncate_prompt_tokens: 0
|
||||
|
||||
- id: builtin-rerank
|
||||
name: bge-reranker-v2-m3
|
||||
type: Rerank
|
||||
source: remote
|
||||
parameters:
|
||||
base_url: ${RERANK_BASE_URL}
|
||||
api_key: ${RERANK_API_KEY}
|
||||
provider: generic
|
||||
```
|
||||
|
||||
#### `${ENV}` 插值
|
||||
|
||||
`api_key` / `base_url` / `name` 等任何**字符串**字段都可以引用环境变量:`${OPENAI_API_KEY}` 会在启动时被对应的 `os.Getenv("OPENAI_API_KEY")` 替换。
|
||||
|
||||
- 环境变量存在 → 替换为实际值
|
||||
- 环境变量不存在 → **保留字面 `${OPENAI_API_KEY}`** 字符串(让 401 错误能直接看出来 env 没设,便于排查)
|
||||
- 不支持 `${VAR:-default}` 这种 shell 扩展,行为与现有 `config.yaml` 的插值实现一致
|
||||
- **非字符串字段不能 env 化**(如 `type`、`dimension`、`is_default`),因为它们必须按 YAML 的目标类型解析
|
||||
|
||||
#### env 变量怎么进入容器
|
||||
|
||||
`docker-compose.yml` 的 `app` 服务已经预置了:
|
||||
|
||||
```yaml
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
```
|
||||
|
||||
意味着把变量值写到项目根目录的 `.env` 文件里,启动时自动透传到容器。**无需**在 `environment:` 块里逐个透传。`required: false` 保证 `.env` 不存在时容器仍可启动(适配上游 fresh clone 场景)。
|
||||
|
||||
仓库的 `.env.example` 顶部预留了 **Built-in Models** 注释段,列出 LLM / Embedding / Rerank 的参考变量名作为起点;复制 `.env.example` 为 `.env` 后取消注释并填值即可。变量名由 YAML 自行决定,参考段只是常见样板,不是保留字。
|
||||
|
||||
完整端到端示例:
|
||||
|
||||
`.env`
|
||||
```bash
|
||||
LLM_MODEL_NAME=gpt-4o-mini
|
||||
LLM_BASE_URL=https://api.openai.com/v1
|
||||
LLM_API_KEY=sk-...
|
||||
LLM_PROVIDER=openai
|
||||
```
|
||||
|
||||
`config/builtin_models.yaml`
|
||||
```yaml
|
||||
builtin_models:
|
||||
- id: builtin-llm-default
|
||||
type: KnowledgeQA
|
||||
is_default: true
|
||||
name: ${LLM_MODEL_NAME}
|
||||
parameters:
|
||||
base_url: ${LLM_BASE_URL}
|
||||
api_key: ${LLM_API_KEY}
|
||||
provider: ${LLM_PROVIDER}
|
||||
```
|
||||
|
||||
启动:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### 启动后验证
|
||||
|
||||
```bash
|
||||
docker compose logs app | grep -E 'Built-in models? config'
|
||||
```
|
||||
|
||||
会看到类似:
|
||||
|
||||
```
|
||||
Built-in model upserted: id=builtin-openai-chat name=gpt-4o-mini type=KnowledgeQA
|
||||
Built-in model upserted: id=builtin-openai-embeddings name=text-embedding-3-small type=Embedding
|
||||
Built-in models config applied: 2 entries from /app/config/builtin_models.yaml.
|
||||
```
|
||||
|
||||
#### Docker 部署
|
||||
|
||||
在 `docker-compose.yml` 的 `app` 服务 `volumes` 块挂载文件:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
volumes:
|
||||
- ./config/builtin_models.yaml:/app/config/builtin_models.yaml:ro
|
||||
```
|
||||
|
||||
仓库提供了 `config/builtin_models.yaml.example` 作为起点,复制为 `config/builtin_models.yaml` 后按需修改。
|
||||
|
||||
### 方式二:直接 SQL 插入
|
||||
|
||||
支持的 provider:`generic`(自定义)、`openai`、`aliyun`、`zhipu`、`volcengine`、`hunyuan`、`deepseek`、`minimax`、`mimo`、`siliconflow`、`jina`、`openrouter`、`gemini`、`modelscope`、`moonshot`、`qianfan`、`qiniu`、`longcat`、`gpustack`
|
||||
|
||||
```sql
|
||||
-- 示例:插入一个 LLM 内置模型
|
||||
-- 示例:LLM 内置模型
|
||||
INSERT INTO models (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
source,
|
||||
description,
|
||||
parameters,
|
||||
is_default,
|
||||
status,
|
||||
is_builtin
|
||||
id, tenant_id, name, type, source, description,
|
||||
parameters, is_default, status, is_builtin
|
||||
) VALUES (
|
||||
'builtin-llm-001', -- 使用固定ID,建议使用 builtin- 前缀
|
||||
10000, -- 租户ID(使用第一个租户)
|
||||
'GPT-4', -- 模型名称
|
||||
'KnowledgeQA', -- 模型类型
|
||||
'remote', -- 模型来源
|
||||
'内置 LLM 模型', -- 描述
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "provider": "openai"}'::jsonb, -- 参数(JSON格式)
|
||||
false, -- 是否默认
|
||||
'active', -- 状态
|
||||
true -- 标记为内置模型
|
||||
'builtin-llm-001',
|
||||
10000,
|
||||
'gpt-4o-mini',
|
||||
'KnowledgeQA',
|
||||
'remote',
|
||||
'系统内置 LLM 模型',
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "provider": "openai"}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 示例:插入一个 Embedding 内置模型
|
||||
-- Embedding
|
||||
INSERT INTO models (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
source,
|
||||
description,
|
||||
parameters,
|
||||
is_default,
|
||||
status,
|
||||
is_builtin
|
||||
id, tenant_id, name, type, source, description,
|
||||
parameters, is_default, status, is_builtin
|
||||
) VALUES (
|
||||
'builtin-embedding-001',
|
||||
10000,
|
||||
'text-embedding-ada-002',
|
||||
'text-embedding-3-small',
|
||||
'Embedding',
|
||||
'remote',
|
||||
'内置 Embedding 模型',
|
||||
'系统内置 Embedding 模型',
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "provider": "openai", "embedding_parameters": {"dimension": 1536, "truncate_prompt_tokens": 0}}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 示例:插入一个 ReRank 内置模型
|
||||
-- Rerank
|
||||
INSERT INTO models (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
source,
|
||||
description,
|
||||
parameters,
|
||||
is_default,
|
||||
status,
|
||||
is_builtin
|
||||
id, tenant_id, name, type, source, description,
|
||||
parameters, is_default, status, is_builtin
|
||||
) VALUES (
|
||||
'builtin-rerank-001',
|
||||
10000,
|
||||
'bge-reranker-base',
|
||||
'bge-reranker-v2-m3',
|
||||
'Rerank',
|
||||
'remote',
|
||||
'内置 ReRank 模型',
|
||||
'系统内置 Rerank 模型',
|
||||
'{"base_url": "https://api.jina.ai/v1", "api_key": "jina-xxx", "provider": "jina"}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 示例:插入一个 VLLM 内置模型
|
||||
INSERT INTO models (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
source,
|
||||
description,
|
||||
parameters,
|
||||
is_default,
|
||||
status,
|
||||
is_builtin
|
||||
) VALUES (
|
||||
'builtin-vllm-001',
|
||||
10000,
|
||||
'gpt-4-vision',
|
||||
'VLLM',
|
||||
'remote',
|
||||
'内置 VLLM 模型',
|
||||
'{"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "api_key": "sk-xxx", "provider": "aliyun"}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
```
|
||||
|
||||
### 3. 验证插入结果
|
||||
|
||||
执行以下 SQL 查询验证内置模型是否成功插入:
|
||||
### 验证插入结果
|
||||
|
||||
```sql
|
||||
SELECT id, name, type, is_builtin, status
|
||||
FROM models
|
||||
SELECT id, name, type, is_builtin, status
|
||||
FROM models
|
||||
WHERE is_builtin = true
|
||||
ORDER BY type, created_at;
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **ID 命名规范**:建议使用 `builtin-{type}-{序号}` 的格式,例如 `builtin-llm-001`、`builtin-embedding-001`
|
||||
2. **租户ID**:内置模型可以属于任意租户,但建议使用第一个租户ID(通常是 10000)
|
||||
3. **参数格式**:`parameters` 字段必须是有效的 JSON 格式
|
||||
4. **幂等性**:使用 `ON CONFLICT (id) DO NOTHING` 确保重复执行不会报错
|
||||
5. **安全性**:内置模型的 API Key 和 Base URL 在前端会被自动隐藏,但数据库中的原始数据仍然存在,请妥善保管数据库访问权限
|
||||
|
||||
## 将现有模型设置为内置模型
|
||||
|
||||
如果你已经有一个模型,想将其设置为内置模型,可以使用 UPDATE 语句:
|
||||
如果你已经手工创建了一个普通模型,想把它升级为内置模型:
|
||||
|
||||
```sql
|
||||
UPDATE models
|
||||
SET is_builtin = true
|
||||
WHERE id = '模型ID' AND name = '模型名称';
|
||||
UPDATE models
|
||||
SET is_builtin = true
|
||||
WHERE id = '模型ID';
|
||||
```
|
||||
|
||||
## 移除内置模型
|
||||
|
||||
如果需要移除内置模型标记(恢复为普通模型),执行:
|
||||
YAML 配置删除条目后**不会**自动清理 DB 里的对应行(避免误删 ops 手工维护的内置模型)。如需删除:
|
||||
|
||||
```sql
|
||||
UPDATE models
|
||||
SET is_builtin = false
|
||||
WHERE id = '模型ID';
|
||||
-- 取消 builtin 标记,恢复为普通模型
|
||||
UPDATE models SET is_builtin = false WHERE id = '模型ID';
|
||||
|
||||
-- 或直接删除
|
||||
DELETE FROM models WHERE id = '模型ID';
|
||||
```
|
||||
|
||||
注意:移除内置模型标记后,该模型将恢复为普通模型,可以被编辑和删除。
|
||||
## 注意事项
|
||||
|
||||
1. **ID 命名规范**:建议使用 `builtin-{type}-{slug}` 的格式,例如 `builtin-openai-chat`、`builtin-rerank`
|
||||
2. **租户ID**:内置模型可以属于任意租户,默认 `10000`(与 `tenants_id_seq` 起点一致)
|
||||
3. **YAML 与 SQL 并存**:两种方式可以同时使用,YAML 不会覆盖通过 SQL 插入但未在 YAML 出现的 builtin 行
|
||||
4. **重启即生效**:修改 YAML 后 `docker compose restart app` 即可让新配置生效
|
||||
5. **加密**:API Key 在 `parameters` JSONB 中以加密形式存储(若 `SYSTEM_AES_KEY` 已配置),未配置时降级为明文兼容路径
|
||||
6. **安全性**:前端会自动隐藏内置模型的 API Key 和 Base URL,但数据库中的原始数据仍然存在,请妥善保管数据库访问权限
|
||||
|
||||
@@ -427,6 +427,24 @@ type FebriText struct {
|
||||
WithNoTag string `yaml:"with_no_tag" json:"with_no_tag"`
|
||||
}
|
||||
|
||||
// resolvedConfigDir holds the directory of the loaded config file. Populated by
|
||||
// LoadConfig and read by ConfigDir(); empty until LoadConfig has run.
|
||||
var resolvedConfigDir string
|
||||
|
||||
// ConfigDir returns the directory containing the loaded config.yaml. Other
|
||||
// startup code (e.g. builtin model loader) uses this to locate sibling config
|
||||
// files like builtin_models.yaml without re-implementing viper search rules.
|
||||
// Falls back to "./config" when LoadConfig has not been called yet.
|
||||
func ConfigDir() string {
|
||||
if resolvedConfigDir != "" {
|
||||
return resolvedConfigDir
|
||||
}
|
||||
if f := viper.ConfigFileUsed(); f != "" {
|
||||
return filepath.Dir(f)
|
||||
}
|
||||
return "./config"
|
||||
}
|
||||
|
||||
// LoadConfig 从配置文件加载配置
|
||||
func LoadConfig() (*Config, error) {
|
||||
// 设置配置文件名和路径
|
||||
@@ -478,6 +496,7 @@ func LoadConfig() (*Config, error) {
|
||||
|
||||
// 加载提示词模板(从目录或配置文件)
|
||||
configDir := filepath.Dir(viper.ConfigFileUsed())
|
||||
resolvedConfigDir = configDir
|
||||
promptTemplates, err := loadPromptTemplates(configDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to load prompt templates from directory: %v\n", err)
|
||||
|
||||
@@ -536,6 +536,11 @@ func initDatabase(cfg *config.Config) (*gorm.DB, error) {
|
||||
// The SQL migration marks KBs that have documents but no provider with "__pending_env__";
|
||||
// we replace that with the actual STORAGE_TYPE from the environment.
|
||||
resolveStorageProviderPending(db)
|
||||
|
||||
// Post-migration: declarative built-in models from config/builtin_models.yaml (optional).
|
||||
if err := types.LoadBuiltinModelsConfig(context.Background(), db, config.ConfigDir()); err != nil {
|
||||
logger.Warnf(context.Background(), "Load builtin models config failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
logger.Infof(context.Background(), "Auto-migration is disabled (AUTO_MIGRATE=false)")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// BuiltinModelEntry mirrors one entry in builtin_models.yaml.
|
||||
// Each entry becomes a row in the models table with is_builtin=true.
|
||||
type BuiltinModelEntry struct {
|
||||
ID string `yaml:"id"`
|
||||
TenantID uint64 `yaml:"tenant_id"`
|
||||
Name string `yaml:"name"`
|
||||
Type ModelType `yaml:"type"`
|
||||
Source ModelSource `yaml:"source"`
|
||||
Description string `yaml:"description"`
|
||||
IsDefault bool `yaml:"is_default"`
|
||||
Status ModelStatus `yaml:"status"`
|
||||
Parameters ModelParameters `yaml:"parameters"`
|
||||
}
|
||||
|
||||
type builtinModelsFile struct {
|
||||
BuiltinModels []BuiltinModelEntry `yaml:"builtin_models"`
|
||||
}
|
||||
|
||||
// builtinModelEnvPattern matches ${NAME} placeholders. Mirrors the pattern in
|
||||
// internal/config/config.go so YAML interpolation behaves identically to the
|
||||
// main config.yaml flow.
|
||||
var builtinModelEnvPattern = regexp.MustCompile(`\${([^}]+)}`)
|
||||
|
||||
// interpolateBuiltinModelEnv substitutes ${NAME} occurrences with the
|
||||
// corresponding os.Getenv value. Unset vars are left as the literal ${NAME}
|
||||
// so misconfiguration surfaces visibly in downstream provider calls instead
|
||||
// of failing silently with an empty token.
|
||||
func interpolateBuiltinModelEnv(s string) string {
|
||||
return builtinModelEnvPattern.ReplaceAllStringFunc(s, func(m string) string {
|
||||
name := m[2 : len(m)-1]
|
||||
if v := os.Getenv(name); v != "" {
|
||||
return v
|
||||
}
|
||||
return m
|
||||
})
|
||||
}
|
||||
|
||||
// LoadBuiltinModelsConfig reads builtin_models.yaml (or the path pointed to by
|
||||
// BUILTIN_MODELS_CONFIG) and UPSERTs each entry into the models table.
|
||||
//
|
||||
// Behaviour:
|
||||
// - file not found / mount point is a directory / path unset: no-op
|
||||
// - YAML parse error: prints a warning, returns nil (never aborts startup)
|
||||
// - per-entry UPSERT error: prints a warning, continues with the next entry
|
||||
// - never deletes entries that disappeared from YAML: operators may have
|
||||
// seeded extras manually via SQL; removing them here would be surprising
|
||||
func LoadBuiltinModelsConfig(ctx context.Context, db *gorm.DB, configDir string) error {
|
||||
path := os.Getenv("BUILTIN_MODELS_CONFIG")
|
||||
if path == "" {
|
||||
path = filepath.Join(configDir, "builtin_models.yaml")
|
||||
}
|
||||
|
||||
// Treat "missing", "is a directory", and other non-regular-file cases the
|
||||
// same way. Docker bind-mounting a non-existent source file silently
|
||||
// substitutes a directory; we don't want that to spam WARN logs.
|
||||
info, statErr := os.Stat(path)
|
||||
if statErr != nil || !info.Mode().IsRegular() {
|
||||
fmt.Printf("Built-in models config not present at %s; skipping.\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: read built-in models config %s failed: %v; skipping.\n", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
expanded := interpolateBuiltinModelEnv(string(raw))
|
||||
|
||||
var file builtinModelsFile
|
||||
if err := yaml.Unmarshal([]byte(expanded), &file); err != nil {
|
||||
fmt.Printf("Warning: parse built-in models config %s failed: %v; skipping.\n", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(file.BuiltinModels) == 0 {
|
||||
fmt.Printf("Built-in models config %s contains no entries.\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
applied := 0
|
||||
for i := range file.BuiltinModels {
|
||||
e := &file.BuiltinModels[i]
|
||||
if e.ID == "" {
|
||||
fmt.Printf("Warning: built-in model entry %d has empty id; skipping.\n", i)
|
||||
continue
|
||||
}
|
||||
m := e.toModel()
|
||||
res := db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"tenant_id", "name", "type", "source", "description",
|
||||
"parameters", "is_default", "status", "is_builtin", "updated_at",
|
||||
}),
|
||||
}).Create(&m)
|
||||
if res.Error != nil {
|
||||
fmt.Printf("Warning: upsert built-in model %s failed: %v; continuing.\n", e.ID, res.Error)
|
||||
continue
|
||||
}
|
||||
applied++
|
||||
fmt.Printf("Built-in model upserted: id=%s name=%s type=%s\n", e.ID, e.Name, e.Type)
|
||||
}
|
||||
fmt.Printf("Built-in models config applied: %d entries from %s.\n", applied, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// toModel converts a YAML entry to a runtime Model with sensible defaults.
|
||||
// tenant_id defaults to 10000 (matches the seed value of tenants_id_seq);
|
||||
// source defaults to "remote"; status defaults to "active". IsBuiltin is
|
||||
// always forced to true regardless of YAML input.
|
||||
func (e *BuiltinModelEntry) toModel() Model {
|
||||
tenantID := e.TenantID
|
||||
if tenantID == 0 {
|
||||
tenantID = 10000
|
||||
}
|
||||
source := e.Source
|
||||
if source == "" {
|
||||
source = ModelSourceRemote
|
||||
}
|
||||
status := e.Status
|
||||
if status == "" {
|
||||
status = ModelStatusActive
|
||||
}
|
||||
return Model{
|
||||
ID: e.ID,
|
||||
TenantID: tenantID,
|
||||
Name: e.Name,
|
||||
Type: e.Type,
|
||||
Source: source,
|
||||
Description: e.Description,
|
||||
Parameters: e.Parameters,
|
||||
IsDefault: e.IsDefault,
|
||||
IsBuiltin: true,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
@@ -164,14 +164,13 @@ func (c *ModelParameters) Scan(value interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeCreate is a GORM hook that runs before creating a new model record
|
||||
// Automatically generates a UUID for new models
|
||||
// Parameters:
|
||||
// - tx: GORM database transaction
|
||||
//
|
||||
// Returns:
|
||||
// - error: Any error encountered during the hook execution
|
||||
// BeforeCreate is a GORM hook that runs before creating a new model record.
|
||||
// Generates a UUID only when the caller has not supplied an ID — preserves
|
||||
// stable IDs declared in built-in model YAML config while keeping the
|
||||
// existing UUID behaviour for API-driven model creation.
|
||||
func (m *Model) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
m.ID = uuid.New().String()
|
||||
if m.ID == "" {
|
||||
m.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user