feat(llm): 本地模型接入 vLLM / Ollama + reasoning 思考过程入轨迹
vLLM 与 Ollama 都暴露 OpenAI 兼容 API(底层 go-openai 请求 {base}/chat/completions),
故统一走 openai 客户端,按 provider 归一化连接参数:
- Ollama/vLLM 的 OpenAI 端点固定在 /v1,BaseURL 漏写自动补全(否则打到 /chat/completions 404)
- 本地后端默认不校验 api_key → 缺省补占位(ollama→"ollama",vllm→"EMPTY";openai 客户端要求非空)
- 显式 key 一律尊重(vLLM --api-key 启动);在线 provider 原样不动(DeepSeek 两种都收)
reasoning_content 适配:ChatStream 增 onReasoning 回调,捕获 reasoning 模型
(本地 Qwen3 思考 / DeepSeek-R1 / QwQ、在线 deepseek-v4-pro)的思考分片。思考阶段分片
Content 为空本就不污染答案;runAgent 把思考累计后 surface 到 exec 轨迹「推理过程」事件。
控制台 ModelManager 加 ollama 选项;llm 包补 normalizeBaseURL / apiKeyOrPlaceholder 单测。
四模块全绿。live:经 admin 配 ollama qwen2.5:0.5b → dispatcher 热切(日志 base 自动 .../v1)
→ POST /v1/chat/completions 200 端到端出答案;切回 deepseek-v4-pro → exec 轨迹现「推理过程:思考26字…」。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
)
|
||||
|
||||
func TestNormalizeBaseURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
provider, in, want string
|
||||
}{
|
||||
{"ollama", "http://localhost:11434", "http://localhost:11434/v1"}, // 补 /v1
|
||||
{"ollama", "http://localhost:11434/", "http://localhost:11434/v1"}, // 去尾斜杠再补
|
||||
{"ollama", "http://localhost:11434/v1", "http://localhost:11434/v1"}, // 已有不重复补
|
||||
{"vllm", "http://gpu-node:8000", "http://gpu-node:8000/v1"},
|
||||
{"vllm", "http://gpu-node:8000/v1", "http://gpu-node:8000/v1"},
|
||||
{"openai-compatible", "https://api.deepseek.com", "https://api.deepseek.com"}, // 在线不动
|
||||
{"", "https://api.deepseek.com", "https://api.deepseek.com"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := normalizeBaseURL(&contract.ModelConfig{Provider: c.provider, BaseURL: c.in})
|
||||
if got != c.want {
|
||||
t.Errorf("normalizeBaseURL(%s,%q)=%q want %q", c.provider, c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIKeyOrPlaceholder(t *testing.T) {
|
||||
// 本地后端缺 key → 占位(openai 客户端要求非空)。
|
||||
if got := apiKeyOrPlaceholder(&contract.ModelConfig{Provider: "ollama"}); got != "ollama" {
|
||||
t.Errorf("ollama 缺 key 应占位 'ollama', got %q", got)
|
||||
}
|
||||
if got := apiKeyOrPlaceholder(&contract.ModelConfig{Provider: "vllm"}); got != "EMPTY" {
|
||||
t.Errorf("vllm 缺 key 应占位 'EMPTY', got %q", got)
|
||||
}
|
||||
// 显式 key 一律尊重(vLLM 带 --api-key 启动的情况)。
|
||||
if got := apiKeyOrPlaceholder(&contract.ModelConfig{Provider: "vllm", APIKey: "sk-real"}); got != "sk-real" {
|
||||
t.Errorf("显式 key 应原样, got %q", got)
|
||||
}
|
||||
// 在线 provider 缺 key 不占位(保持空,由 Ready/调用方处理)。
|
||||
if got := apiKeyOrPlaceholder(&contract.ModelConfig{Provider: "openai-compatible"}); got != "" {
|
||||
t.Errorf("在线 provider 缺 key 不应占位, got %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user