package main import ( "path/filepath" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/multimodal/vision" ) // visionToolName and ocrToolName are the registered names of the vision and OCR // tools. They are bound to the tool registration here and advertised to the // agent by the tool dispatcher. const ( visionToolName = "vision" ocrToolName = "ocr" ) // wireVision registers the vision and OCR tools and the vision model routing. // // The vision adapter is an OpenAI-compatible /chat/completions endpoint on the // same base URL as the chat LLM (LLM_BASE_URL), using the dedicated model // (LLM_VISION_MODEL) when set. When no base URL is configured the tools are // skipped with a warning so startup never blocks; the model router is still // bound to the vision model (in wireRuntime) whenever one is configured. func (a *app) wireVision() { log := logger.WithSource(a.log, "cmd/cyrene") if strings.TrimSpace(a.cfg.LLM.BaseURL) == "" { log.Warn("vision/ocr tools skipped: no LLM base url configured") return } adapter := vision.NewOpenAICompatible(a.cfg.LLM, vision.WithModel(a.cfg.LLM.VisionModel), // 同源 upload 产物(`/api/v1/files/`)走**本地读取**:与自己取回相比更快, // 且在「服务端路由不到自己的广告 host」(如安卓模拟器 10.0.2.2、反向代理改写) // 的部署下才取得到图。目录必须与 upload 的落盘位置一致 = `/uploads`。 vision.WithUploadsDir(filepath.Join(resolveDataDir(a.cfg), "uploads")), vision.WithLogger(a.log)) if err := vision.RegisterTools(a.caps, vision.NewVisionTool(visionToolName, adapter), vision.NewOCRTool(ocrToolName, adapter), ); err != nil { log.Warn("vision/ocr tool register skipped", "error", err) return } log.Info("vision/ocr tools registered", "vision_model", a.cfg.LLM.VisionModel) }