Files
deskview/internal/llm/provider.go
T
2026-10-28 08:47:09 +08:00

125 lines
3.3 KiB
Go

// Package llm 提供大模型通信能力(含视觉与流式),
// 支持 OpenAI Chat Completions 与 Anthropic Messages 两种接口格式。
package llm
import (
"context"
"fmt"
"net/http"
"time"
)
// 接口格式。它决定请求体结构、鉴权头与流式事件的解析方式,
// 也是配置里唯一需要用户先选的东西。
const (
// FormatOpenAI 是 OpenAI Chat Completions 兼容格式(POST {base}/chat/completions)。
FormatOpenAI = "openai"
// FormatAnthropic 是 Anthropic Messages 兼容格式(POST {base}/v1/messages)。
FormatAnthropic = "anthropic"
)
// FormatLabel 返回接口格式的中文名,供界面显示。
func FormatLabel(format string) string {
switch format {
case FormatAnthropic:
return "Anthropic 兼容"
default:
return "OpenAI 兼容"
}
}
// Formats 返回全部受支持的接口格式,顺序即界面下拉顺序。
func Formats() []string { return []string{FormatOpenAI, FormatAnthropic} }
// Role 是消息角色。
type Role string
// 支持的角色。
const (
RoleUser Role = "user"
RoleAssistant Role = "assistant"
)
// Message 是一条对话消息。Image 非空表示该消息附带一张 JPEG 图片。
type Message struct {
Role Role
Text string
Image []byte
}
// Usage 记录一次调用的 token 用量。
type Usage struct {
PromptTokens int
CompletionTokens int
TotalTokens int
}
// Add 累加用量。
func (u *Usage) Add(o Usage) {
u.PromptTokens += o.PromptTokens
u.CompletionTokens += o.CompletionTokens
u.TotalTokens += o.TotalTokens
}
// Delta 是一次流式增量。
// Text 是正式回答,Reasoning 是模型吐出来的思考过程;两者都按原样透传,
// 服务端吐什么、界面就显示什么。
type Delta struct {
Text string
Reasoning string
}
// DeltaFunc 接收流式增量,返回 error 可中断请求。
type DeltaFunc func(delta Delta) error
// Options 描述一个模型的接入参数。
type Options struct {
Name string
Format string
BaseURL string
APIKey string
Model string
ExtraHeaders map[string]string
MaxTokens int
Temperature float64
}
// Provider 是大模型服务的最小抽象。
type Provider interface {
// Chat 发起一次流式对话,system 为系统提示词。
Chat(ctx context.Context, system string, msgs []Message, fn DeltaFunc) (Usage, error)
// Ping 做一次最小代价的连通性检查。
Ping(ctx context.Context) error
}
// New 根据模型配置创建 Provider,按 Format 分派到对应协议实现。
func New(opts Options) (Provider, error) {
if opts.BaseURL == "" {
return nil, fmt.Errorf("模型配置缺少 BaseURL")
}
if opts.Model == "" {
return nil, fmt.Errorf("模型 %s 缺少模型名", opts.Name)
}
switch opts.Format {
case "", FormatOpenAI:
return newOpenAI(opts), nil
case FormatAnthropic:
return newAnthropic(opts), nil
default:
return nil, fmt.Errorf("未知的接口格式 %q", opts.Format)
}
}
// newHTTPClient 返回不设总超时的客户端:流式响应可能持续很久,由 ctx 控制。
func newHTTPClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: 8,
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 15 * time.Second,
},
}
}