// Package gemini 实现 Google Gemini API 的 ModelProvider. // // 支持两种接入方式: // 1. Google AI Studio - 通过 API key(?key=... 查询参数)访问 // 入口:https://aistudio.google.com/,免费额度慷慨,适合开发测试 // 2. Vertex AI - 通过 GCP OAuth2 Bearer token 访问 // 入口:https://cloud.google.com/vertex-ai,企业级 SLA,支持 VPC Service Controls // // 两种模式由 Config.BearerToken 区分: // - BearerToken 为空 → Google AI Studio(使用 APIKey) // - BearerToken 非空 → Vertex AI(APIKey 被忽略) // // 升华改进(ELEVATED): 早期实现 无 Gemini provider--首次实现. // Gemini 是第三种 SSE 协议,与 Anthropic / OpenAI 均不同: // 每块是完整 GenerateContentResponse,thinking 用 thought: true 标记, // functionCall 完整到达无需拼接 arguments. // wire.GeminiClient 封装了全部协议差异,本包只负责配置工厂和维护静态模型表. package gemini import ( "context" "net/http" "time" "git.flytoex.net/yuanwei/flyto-agent/internal/wire" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" "git.flytoex.net/yuanwei/flyto-agent/pkg/providers/shared" ) // Config 是 Gemini provider 的配置. type Config struct { // APIKey 是 Google AI Studio API 密钥(从 aistudio.google.com 获取). // Vertex AI 模式下此字段被忽略. APIKey string // BearerToken 是 Vertex AI 的 GCP OAuth2 Bearer token. // 非空时切换为 Vertex AI 模式,APIKey 不再使用. // 生产环境建议用 google.golang.org/api/oauth2 自动刷新 token, // 但引擎核心不引入外部依赖,由调用方在外部刷新后注入. BearerToken string // BaseURL 覆盖 API 端点. // Google AI 默认:https://generativelanguage.googleapis.com // Vertex AI 示例:https://us-central1-aiplatform.googleapis.com/v1/projects/my-project/locations/us-central1/publishers/google BaseURL string // ThinkingBudget 启用扩展思考(Gemini 2.5+,-exp 标签模型). // 0 = 禁用;建议值 1024–16384. // 通过 generationConfig.thinkingConfig.thinkingBudget 传递. ThinkingBudget int // HTTPClient 注入自定义 HTTP 客户端(代理,超时等). // nil = 使用默认 http.Client (带 Timeout 字段配置的 ResponseHeaderTimeout). // 非 nil 时 consumer 完全接管超时责任, 下面的 Timeout 字段被忽略. HTTPClient *http.Client // Timeout 限制"从请求发出到收到响应首字节"的时间. // // 通过 http.Transport.ResponseHeaderTimeout 实现, **不影响** SSE 流式响应的后续 // body 读取 - 长流式回复 (2-5 分钟) 可以正常读完. // // 精妙之处(CLEVER): 不要误用 http.Client.Timeout - 那会把 SSE 流砍死. // 详见 internal/wire/gemini.go 和 anthropic.go §3.1 完整论证. // // 0 = 使用 defaultTimeout (60s, 适合 Gemini 云端 AI Studio / Vertex AI). // 仅当 HTTPClient 为 nil 时生效; 提供自定义 HTTPClient 时此字段被忽略. Timeout time.Duration // ModelOverrides 覆盖静态模型表. // 用于 Google 新发布但我们尚未维护的模型. ModelOverrides []flyto.ModelInfo } // defaultTimeout 是 Config.Timeout == 0 且 Config.HTTPClient == nil 时的 ResponseHeaderTimeout 兜底值. // 60s 对云端 Gemini 合理: Gemini 模型通常 < 10s 开始响应, 60s 留充足余量给网络抖动 / Vertex AI 路由. const defaultTimeout = 60 * time.Second // GoString 实现 fmt.GoStringer,防止 %#v 打印时泄露 APIKey 或 BearerToken. // 升华改进(ELEVATED): Gemini 有双密钥路径(AI Studio APIKey vs Vertex BearerToken),两个都要遮蔽. // 替代方案:<只遮蔽当前激活路径的密钥> - 当前两个都遮蔽,更安全且实现简单. func (c Config) GoString() string { return shared.GoStringWithMaskedKey("Config", c.APIKey) } // Provider 是 Gemini ModelProvider 实现. type Provider struct { cfg Config client *wire.GeminiClient } // New 创建 Gemini ModelProvider. func New(cfg Config) *Provider { var opts []wire.GeminiOption // 超时配置: HTTPClient 和 Timeout 二选一, 不混用. // 精妙之处(CLEVER): 二选一而非叠加 - 消费者注入 HTTPClient 就是完全接管超时策略, // 我们不去 mutate 他的 Transport (rude).消费者不注入, 我们用 Timeout 字段 // (或 defaultTimeout 兜底) 配置 ResponseHeaderTimeout. if cfg.HTTPClient != nil { opts = append(opts, wire.GeminiWithHTTPClient(cfg.HTTPClient)) } else { timeout := cfg.Timeout if timeout == 0 { timeout = defaultTimeout } opts = append(opts, wire.GeminiWithResponseHeaderTimeout(timeout)) } if cfg.ThinkingBudget > 0 { opts = append(opts, wire.GeminiWithThinkingBudget(cfg.ThinkingBudget)) } if cfg.BearerToken != "" { opts = append(opts, wire.GeminiWithBearerToken(cfg.BearerToken)) } return &Provider{ cfg: cfg, client: wire.NewGeminiClient(cfg.APIKey, cfg.BaseURL, opts...), } } var _ flyto.ModelProvider = (*Provider)(nil) // Name 返回 provider 标识. func (p *Provider) Name() string { return "gemini" } // geminiMaxTools 是 Gemini 工具数量上限的**兜底安全值**(0 = Gemini API 文档无显式上限). // // 升华改进(ELEVATED): data-driven-capabilities RFC PR2.4 - 主决策来自 req.Capabilities.MaxTools. // Gemini 官方文档截至 2026-04 未公布单请求 tool 数量上限,因此兜底 0 = 不检查 // (CheckToolCount 跳过).未来 capability-probe 若实测出上限可通过 registry 注入. const geminiMaxTools = 0 // resolveMaxTools 返回当前请求的工具上限 + 是否为硬性上限(Exhaustive). // registry 优先 + 包内 geminiMaxTools 兜底. // // 零回归保证:nil Capabilities → 返回 (0, false) → Stream 不进入 check 分支, // 行为完全等同 PR2.4 之前(早期方案 gemini 无 max_tools 检查). func resolveMaxTools(req *flyto.Request) (int, bool) { if req.Capabilities != nil && req.Capabilities.MaxTools > 0 { return req.Capabilities.MaxTools, req.Capabilities.MaxToolsExhaustive } return geminiMaxTools, false } // modelSupportsThinking 返回指定模型是否支持扩展思考(从静态表查找). // // 升华改进(ELEVATED): PR2.4 从 Provider method 改为包级函数, // 与其他 provider 的 helper 风格统一,且避免 resolveThinkingSupport 需要持有 *Provider 引用. func modelSupportsThinking(model string) bool { for _, m := range geminiModels { if m.ID == model { return m.SupportsThinking } } // 未知模型保守返回 false,避免为不支持 thinking 的模型注入 thinking budget return false } // resolveThinkingSupport 返回当前请求模型是否支持 thinking.registry 优先 + 静态表兜底. // // 升华改进(ELEVATED): data-driven-capabilities RFC PR2.4 - 单一真相源. // Stream 用于决定是否清零 thinking budget,detectFeatureWarnings 用于判断 silent disable, // 两处都调用本 helper,确保判断一致. func resolveThinkingSupport(req *flyto.Request) bool { if req.Capabilities != nil { return req.Capabilities.SupportsThinking } return modelSupportsThinking(req.Model) } // detectFeatureWarnings 检测 want × can 不一致的场景,返回 WarningEvent 列表. // // 升华改进(ELEVATED): data-driven-capabilities RFC §4.4 双开关协议. // gemini 早期方案代码 (Stream 内) 存在一处教科书级的 silent disable: // // if thinkingBudget > 0 && !modelSupportsThinking(req.Model) { thinkingBudget = 0 } // // 用户设置 ThinkingBudget 或 NeedsThinking,但模型不支持时 budget 被静默清零. // PR2.4 保留清零行为(零回归),但同时通过 WarningEvent 把 silent 转为 loud, // 让消费者(engine/observer/TUI)能看到这种降级. // // gemini 当前仅检测 thinking;Config 无 EnableCaching 字段(wire 层不处理 caching), // 所以 caching 的 want 永远为 false,不触发检测. func (p *Provider) detectFeatureWarnings(req *flyto.Request) []*flyto.WarningEvent { var warnings []*flyto.WarningEvent wantsThinking := p.cfg.ThinkingBudget > 0 || req.NeedsThinking if wantsThinking && !resolveThinkingSupport(req) { warnings = append(warnings, &flyto.WarningEvent{ Code: "feature_unsupported", Message: "Config.ThinkingBudget/NeedsThinking 已设置但模型 " + req.Model + " 不支持 thinking,thinking 已被忽略", Detail: "model=" + req.Model + " feature=thinking", }) } return warnings } // prependWarnings 在下游 channel 前面 prepend 一组 WarningEvent. // 参见 anthropic/ollama/openai 同款实现;RFC §3 决策不抽共享包,duplication 可接受. func prependWarnings(downstream <-chan flyto.Event, warnings []*flyto.WarningEvent) <-chan flyto.Event { out := make(chan flyto.Event, len(warnings)) for _, w := range warnings { out <- w } go func() { defer close(out) for evt := range downstream { out <- evt } }() return out } // Stream 向 Gemini API 发起流式请求. // // 内部使用 wire.GeminiClient,SSE 解析逻辑见 internal/wire/gemini.go. func (p *Provider) Stream(ctx context.Context, req *flyto.Request) (<-chan flyto.Event, error) { // Vision guard: Gemini provider 尚未接入 BlockImage wire. 对齐 openai // provider 同构守卫 -- 遇 image block 显式报错不静默丢弃. // // Gemini provider has not yet wired BlockImage. Mirrors the openai // provider guard -- surface an error instead of silent-dropping. if err := shared.CheckNoImageBlocks(req.Messages, "gemini"); err != nil { return nil, err } // CAP-7: Tool 数量上限检查 - data-driven-capabilities RFC PR2.4(新增路径). // 兜底 geminiMaxTools=0 表示 Gemini 文档无显式上限;registry 可注入实测上限. // 仅 Exhaustive=true 时客户端硬拒,避免误伤 probe 覆盖不全的模型. maxTools, exhaustive := resolveMaxTools(req) if maxTools > 0 && exhaustive && len(req.Tools) > maxTools { if err := wire.CheckToolCount(req.Tools, maxTools); err != nil { return nil, err } } // 双开关协议(want × can)检测 - 必须在 thinking 清零**之前**计算, // 因为清零后就看不出用户的 want 了. warnings := p.detectFeatureWarnings(req) thinkingBudget := p.cfg.ThinkingBudget if thinkingBudget == 0 && req.NeedsThinking { thinkingBudget = 8000 } // silent disable 清零保持行为(零回归),warning 已在上方 detect 过, // 消费者会看到 silent 的 loud 版本. if thinkingBudget > 0 && !resolveThinkingSupport(req) { thinkingBudget = 0 } ch, err := p.client.Stream(ctx, &wire.StreamRequest{ Model: req.Model, Messages: req.Messages, System: req.System, MaxTokens: req.MaxTokens, Tools: req.Tools, ThinkingBudget: thinkingBudget, ResponseFormat: req.ResponseFormat, Temperature: req.Temperature, TopP: req.TopP, }) if err != nil { return nil, err } if len(warnings) > 0 { return prependWarnings(ch, warnings), nil } return ch, nil } // Models 返回 Gemini 可用模型列表(静态表). func (p *Provider) Models(_ context.Context) ([]flyto.ModelInfo, error) { if len(p.cfg.ModelOverrides) > 0 { return p.cfg.ModelOverrides, nil } return geminiModels, nil } // geminiModels 是 Gemini 主要模型静态表(2026-04 更新). // 价格单位:USD / 1M tokens(Google AI Studio 定价,Vertex AI 可能不同). // // 上下文窗口说明: // - Gemini 2.5 系列支持 1M tokens 上下文(部分场景需开启 long context mode) // - 所有模型均支持视觉输入(图像,视频帧) var geminiModels = []flyto.ModelInfo{ { ID: "gemini-2.5-pro", DisplayName: "Gemini 2.5 Pro", Provider: "gemini", ContextWindow: 1_000_000, MaxOutputTokens: 65_536, InputPricePer1M: 1.25, // ≤200K tokens OutputPricePer1M: 10.0, SupportsCaching: true, SupportsThinking: true, SupportsVision: true, }, { ID: "gemini-2.5-flash", DisplayName: "Gemini 2.5 Flash", Provider: "gemini", ContextWindow: 1_000_000, MaxOutputTokens: 65_536, InputPricePer1M: 0.15, OutputPricePer1M: 0.60, SupportsCaching: true, SupportsThinking: true, SupportsVision: true, }, { ID: "gemini-2.5-flash-thinking-exp", DisplayName: "Gemini 2.5 Flash Thinking (Experimental)", Provider: "gemini", ContextWindow: 1_000_000, MaxOutputTokens: 65_536, InputPricePer1M: 0.15, OutputPricePer1M: 0.60, SupportsCaching: false, // 实验模型暂不支持缓存 SupportsThinking: true, SupportsVision: true, }, { ID: "gemini-2.0-flash", DisplayName: "Gemini 2.0 Flash", Provider: "gemini", ContextWindow: 1_000_000, MaxOutputTokens: 8_192, InputPricePer1M: 0.10, OutputPricePer1M: 0.40, SupportsCaching: true, SupportsThinking: false, SupportsVision: true, }, { ID: "gemini-2.0-flash-thinking-exp", DisplayName: "Gemini 2.0 Flash Thinking (Experimental)", Provider: "gemini", ContextWindow: 1_000_000, MaxOutputTokens: 8_192, InputPricePer1M: 0.10, OutputPricePer1M: 0.40, SupportsCaching: false, SupportsThinking: true, SupportsVision: true, }, { ID: "gemini-1.5-pro", DisplayName: "Gemini 1.5 Pro", Provider: "gemini", ContextWindow: 2_000_000, MaxOutputTokens: 8_192, InputPricePer1M: 1.25, // ≤128K tokens OutputPricePer1M: 5.0, SupportsCaching: true, SupportsThinking: false, SupportsVision: true, }, { ID: "gemini-1.5-flash", DisplayName: "Gemini 1.5 Flash", Provider: "gemini", ContextWindow: 1_000_000, MaxOutputTokens: 8_192, InputPricePer1M: 0.075, OutputPricePer1M: 0.30, SupportsCaching: true, SupportsThinking: false, SupportsVision: true, }, { ID: "gemini-1.5-flash-8b", DisplayName: "Gemini 1.5 Flash 8B", Provider: "gemini", ContextWindow: 1_000_000, MaxOutputTokens: 8_192, InputPricePer1M: 0.0375, OutputPricePer1M: 0.15, SupportsCaching: true, SupportsThinking: false, SupportsVision: true, }, }