feat: add openrouter
This commit is contained in:
@@ -39,9 +39,9 @@ func (c *Config) ValidateBase() error {
|
|||||||
return fmt.Errorf("api key is required (set OPENAI_API_KEY or ANTHROPIC_API_KEY)")
|
return fmt.Errorf("api key is required (set OPENAI_API_KEY or ANTHROPIC_API_KEY)")
|
||||||
}
|
}
|
||||||
switch c.Provider {
|
switch c.Provider {
|
||||||
case "openai", "anthropic", "gemini":
|
case "openai", "anthropic", "gemini", "openrouter":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported provider %q (use openai/anthropic/gemini)", c.Provider)
|
return fmt.Errorf("unsupported provider %q (use openai/anthropic/gemini/openrouter)", c.Provider)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
26
app/run.go
26
app/run.go
@@ -5,8 +5,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -43,6 +45,11 @@ func Run(cfg Config, refs tools.References, prompts Prompts, styles map[string]s
|
|||||||
return fmt.Errorf("init store: %w", err)
|
return fmt.Errorf("init store: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1.5 日志写入文件(CLI 模式同时输出到 stderr 和日志文件)
|
||||||
|
if cleanup := setupFileLogger(store.Dir()); cleanup != nil {
|
||||||
|
defer cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 创建模型
|
// 2. 创建模型
|
||||||
model, err := createModel(cfg)
|
model, err := createModel(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -505,6 +512,23 @@ func finalizeSteerIfIdle(store *state.Store) {
|
|||||||
clearHandledSteer(store)
|
clearHandledSteer(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupFileLogger 设置 CLI 模式日志,同时输出到 stderr 和日志文件。
|
||||||
|
func setupFileLogger(outputDir string) func() {
|
||||||
|
logPath := filepath.Join(outputDir, "meta", "cli.log")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.SetOutput(io.MultiWriter(os.Stderr, f))
|
||||||
|
return func() {
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
_ = f.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// createModel 根据 provider 创建对应的 LLM 模型。
|
// createModel 根据 provider 创建对应的 LLM 模型。
|
||||||
func createModel(cfg Config) (agentcore.ChatModel, error) {
|
func createModel(cfg Config) (agentcore.ChatModel, error) {
|
||||||
var baseURL []string
|
var baseURL []string
|
||||||
@@ -516,7 +540,7 @@ func createModel(cfg Config) (agentcore.ChatModel, error) {
|
|||||||
return llm.NewAnthropicModel(cfg.ModelName, cfg.APIKey, baseURL...)
|
return llm.NewAnthropicModel(cfg.ModelName, cfg.APIKey, baseURL...)
|
||||||
case "gemini":
|
case "gemini":
|
||||||
return llm.NewGeminiModel(cfg.ModelName, cfg.APIKey, baseURL...)
|
return llm.NewGeminiModel(cfg.ModelName, cfg.APIKey, baseURL...)
|
||||||
default:
|
default: // openai, openrouter 及其他 OpenAI 兼容服务
|
||||||
return llm.NewOpenAIModel(cfg.ModelName, cfg.APIKey, baseURL...)
|
return llm.NewOpenAIModel(cfg.ModelName, cfg.APIKey, baseURL...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
main.go
12
main.go
@@ -46,15 +46,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildConfig(style string) app.Config {
|
func buildConfig(style string) app.Config {
|
||||||
provider := envOr("LLM_PROVIDER", "openai")
|
provider := envOr("LLM_PROVIDER", "openrouter")
|
||||||
apiKey := os.Getenv("Z_OPENAI_API_KEY")
|
apiKey := os.Getenv("Z_OPENAI_API_KEY")
|
||||||
baseURL := os.Getenv("Z_OPENAI_BASE_URL")
|
baseURL := os.Getenv("Z_OPENAI_BASE_URL")
|
||||||
if provider == "anthropic" {
|
switch provider {
|
||||||
|
case "anthropic":
|
||||||
apiKey = envOr("ANTHROPIC_API_KEY", apiKey)
|
apiKey = envOr("ANTHROPIC_API_KEY", apiKey)
|
||||||
baseURL = envOr("ANTHROPIC_BASE_URL", baseURL)
|
baseURL = envOr("ANTHROPIC_BASE_URL", baseURL)
|
||||||
} else if provider == "gemini" {
|
case "gemini":
|
||||||
apiKey = envOr("GEMINI_API_KEY", apiKey)
|
apiKey = envOr("GEMINI_API_KEY", apiKey)
|
||||||
baseURL = envOr("GEMINI_BASE_URL", baseURL)
|
baseURL = envOr("GEMINI_BASE_URL", baseURL)
|
||||||
|
case "openrouter":
|
||||||
|
apiKey = envOr("OPENROUTER_API_KEY", apiKey)
|
||||||
|
baseURL = envOr("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := app.Config{
|
cfg := app.Config{
|
||||||
@@ -62,7 +66,7 @@ func buildConfig(style string) app.Config {
|
|||||||
Provider: provider,
|
Provider: provider,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
ModelName: envOr("MODEL_NAME", ""),
|
ModelName: "stepfun/step-3.5-flash:free",
|
||||||
Style: style,
|
Style: style,
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ func (m *Model) updateViewportSize() {
|
|||||||
bodyH := m.bodyHeight()
|
bodyH := m.bodyHeight()
|
||||||
eventH, streamH := m.splitHeights(bodyH)
|
eventH, streamH := m.splitHeights(bodyH)
|
||||||
m.viewport.Width = centerW - 2
|
m.viewport.Width = centerW - 2
|
||||||
m.viewport.Height = eventH
|
m.viewport.Height = eventH - 1 // -1 为 event panel header 行
|
||||||
m.streamVP.Width = centerW - 2
|
m.streamVP.Width = centerW - 2
|
||||||
m.streamVP.Height = streamH - 1 // -1 为 stream panel header 行
|
m.streamVP.Height = streamH - 1 // -1 为 stream panel header 行
|
||||||
}
|
}
|
||||||
@@ -366,16 +366,16 @@ func (m Model) View() string {
|
|||||||
centerW := m.width - leftW - rightW
|
centerW := m.width - leftW - rightW
|
||||||
eventH, streamH := m.splitHeights(bodyH)
|
eventH, streamH := m.splitHeights(bodyH)
|
||||||
|
|
||||||
if m.viewport.Width != centerW-2 || m.viewport.Height != eventH {
|
if m.viewport.Width != centerW-2 || m.viewport.Height != eventH-1 {
|
||||||
m.viewport.Width = centerW - 2
|
m.viewport.Width = centerW - 2
|
||||||
m.viewport.Height = eventH
|
m.viewport.Height = eventH - 1 // -1 为 event panel header 行
|
||||||
}
|
}
|
||||||
if m.streamVP.Width != centerW-2 || m.streamVP.Height != streamH-1 {
|
if m.streamVP.Width != centerW-2 || m.streamVP.Height != streamH-1 {
|
||||||
m.streamVP.Width = centerW - 2
|
m.streamVP.Width = centerW - 2
|
||||||
m.streamVP.Height = streamH - 1 // -1 为 stream panel header 行
|
m.streamVP.Height = streamH - 1 // -1 为 stream panel header 行
|
||||||
}
|
}
|
||||||
|
|
||||||
eventFlow := renderEventFlowViewport(m.viewport, centerW, eventH)
|
eventFlow := renderEventFlowViewport(m.viewport, centerW, eventH, !m.focusStream)
|
||||||
streamPanel := renderStreamPanel(m.streamVP, centerW, streamH, m.focusStream)
|
streamPanel := renderStreamPanel(m.streamVP, centerW, streamH, m.focusStream)
|
||||||
center := lipgloss.JoinVertical(lipgloss.Left, eventFlow, streamPanel)
|
center := lipgloss.JoinVertical(lipgloss.Left, eventFlow, streamPanel)
|
||||||
|
|
||||||
|
|||||||
@@ -170,13 +170,30 @@ func renderEventContent(events []app.UIEvent, width int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// renderEventFlowViewport 用 viewport 包装渲染事件流面板。
|
// renderEventFlowViewport 用 viewport 包装渲染事件流面板。
|
||||||
func renderEventFlowViewport(vp viewport.Model, width, height int) string {
|
func renderEventFlowViewport(vp viewport.Model, width, height int, focused bool) string {
|
||||||
|
// 标题栏
|
||||||
|
titleColor := colorDim
|
||||||
|
if focused {
|
||||||
|
titleColor = colorAccent
|
||||||
|
}
|
||||||
|
title := lipgloss.NewStyle().Foreground(titleColor).Render("✦ 事件流")
|
||||||
|
lineW := width - lipgloss.Width(title) - 4
|
||||||
|
if lineW < 0 {
|
||||||
|
lineW = 0
|
||||||
|
}
|
||||||
|
separator := lipgloss.NewStyle().Foreground(colorDim).Render(strings.Repeat("─", lineW))
|
||||||
|
header := " " + title + " " + separator
|
||||||
|
|
||||||
|
vpH := height - 1
|
||||||
|
if vpH < 1 {
|
||||||
|
vpH = 1
|
||||||
|
}
|
||||||
style := lipgloss.NewStyle().
|
style := lipgloss.NewStyle().
|
||||||
Width(width).
|
Width(width).
|
||||||
Height(height).
|
Height(vpH).
|
||||||
Padding(0, 1)
|
Padding(0, 1)
|
||||||
|
|
||||||
return style.Render(vp.View())
|
return header + "\n" + style.Render(vp.View())
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderStreamPanel 渲染流式输出面板(中间列下半部分)。
|
// renderStreamPanel 渲染流式输出面板(中间列下半部分)。
|
||||||
|
|||||||
Reference in New Issue
Block a user