// plexum-gemini-provider-plugin is a Plexum ProviderPlugin that wraps // the local `gemini` CLI binary (Google Gemini CLI). Each Plexum turn // forks one `gemini -p` subprocess in the agent's workspace, captures // JSON output, and emits canonical TurnEvents. Multi-turn continuity // via `--resume ` (session id stashed at // /.plexum-gemini-session). // // Reference: openclaw/extensions/google/cli-backend.ts. Same CLI-driven // approach as Plexum-anthropic-provider's contractor mode. // // Models — Plexum agent.json.model can be any of: // gemini-pro → CLI alias "pro" → gemini-3.1-pro-preview // gemini-flash → CLI alias "flash" → gemini-3.1-flash-preview (default) // gemini-flash-lite → CLI alias "flash-lite" → gemini-3.1-flash-lite-preview // gemini → no --model flag (gemini-cli default) // or any raw model id the local gemini CLI accepts (passed through). package main import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "git.hangman-lab.top/hzhang/Plexum-sdk-go/canonical" plugin "git.hangman-lab.top/hzhang/Plexum-sdk-go/plugin" "git.hangman-lab.top/hzhang/Plexum-gemini-provider/internal/runner" ) const pluginName = "plexum-gemini-provider" // Aliases the plugin translates before passing --model to the CLI. // CLI accepts the aliases directly too, but normalizing keeps logs // consistent across operator + provider sides. var modelAliases = map[string]string{ "gemini-pro": "pro", "gemini-flash": "flash", "gemini-flash-lite": "flash-lite", } // supportedModels: what the manifest's provider.models advertises. // Operator agent.json.model must match one of these; we route the // "gemini" plain id to the CLI's default (no --model flag). var supportedModels = []string{ "gemini", "gemini-pro", "gemini-flash", "gemini-flash-lite", } // HostConfig is per-profile plugin config at // /plugins/plexum-gemini-provider/config.json. All fields // optional — sensible defaults if file is missing. // // { // "binary": "gemini", // executable name or absolute path // "extra_args": ["--yolo"] // appended after the standard flags // } // // No api_key field: gemini CLI auth lives in ~/.gemini (OAuth or // GEMINI_API_KEY env), already set up by the operator. type HostConfig struct { Binary string `json:"binary"` ExtraArgs []string `json:"extra_args"` } type geminiPlugin struct { host plugin.HostAPI cfg HostConfig } func (p *geminiPlugin) Manifest() plugin.Manifest { return plugin.Manifest{ Name: pluginName, Version: "0.1.0", Activation: plugin.ActivationLazy, Executable: "plexum-gemini-provider-plugin", Contracts: plugin.Contracts{ Provider: &plugin.ProviderContract{Models: supportedModels}, }, } } func (p *geminiPlugin) Init(ctx context.Context, host plugin.HostAPI) error { p.host = host profileRoot := os.Getenv("PLEXUM_PROFILE_ROOT") if profileRoot == "" { home, _ := os.UserHomeDir() profileRoot = filepath.Join(home, ".plexum") } cfgPath := filepath.Join(profileRoot, "plugins", pluginName, "config.json") raw, err := os.ReadFile(cfgPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("read %s: %w", cfgPath, err) } if len(raw) > 0 { if err := json.Unmarshal(raw, &p.cfg); err != nil { return fmt.Errorf("parse %s: %w", cfgPath, err) } } if p.cfg.Binary == "" { p.cfg.Binary = "gemini" } host.Log("info", "gemini provider initialized", map[string]any{ "binary": p.cfg.Binary, "extra_args": p.cfg.ExtraArgs, "models": supportedModels, }) return nil } // Stream is the bare ProviderPlugin entrypoint; called when the SDK // can't see StreamWithAgent. We use the agent-aware variant because // the CLI needs the workspace cwd. func (p *geminiPlugin) Stream(ctx context.Context, modelID string, req canonical.TurnRequest) (<-chan canonical.TurnEvent, error) { return p.StreamWithAgent(ctx, modelID, plugin.AgentContext{}, req) } // StreamWithAgent receives the AgentContext (workspace path) Plexum's // host wires in for this kind of plugin (see ProviderPluginWithAgent). func (p *geminiPlugin) StreamWithAgent(ctx context.Context, modelID string, agent plugin.AgentContext, req canonical.TurnRequest) (<-chan canonical.TurnEvent, error) { cliModel := mapModel(modelID) return runner.Run(ctx, p.host, agent, cliModel, p.cfg.Binary, p.cfg.ExtraArgs, req) } // mapModel converts a Plexum-advertised model id into the CLI's // --model value. Returns "" for "gemini" (use CLI default) and // passes any other unrecognized id through verbatim (operator may // have configured a specific gemini-3.x model id directly). func mapModel(plexumModel string) string { if plexumModel == "" || plexumModel == "gemini" { return "" } if alias, ok := modelAliases[plexumModel]; ok { return alias } return plexumModel } func main() { if err := plugin.Serve(&geminiPlugin{}); err != nil { fmt.Fprintf(os.Stderr, "plexum-gemini-provider-plugin: %v\n", err) os.Exit(1) } }