// plexum-openai-provider-plugin wraps the local `codex` CLI binary // (OpenAI Codex CLI ≥ 0.135). Same CLI-driven approach as // Plexum-gemini-provider — each Plexum turn forks one `codex exec` // subprocess in the agent's workspace, captures JSONL output, and // emits canonical TurnEvents. // // Auth: codex CLI's own ~/.codex/ state (ChatGPT login or // OPENAI_API_KEY env). No api_key field in this plugin's config. // // Models: by default only "codex" (use whatever the CLI's bound // account supports — ChatGPT login restricts model selection). If // running on an API key, operator can add other ids via config. 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-openai-provider/internal/runner" ) const pluginName = "plexum-openai-provider" // Default model surface. "codex" → no --model flag (CLI default). // Operator can extend via config.SupportedModels override (e.g. // "gpt-5", "o3" — only valid on API-key accounts). var defaultModels = []string{"codex"} // HostConfig at /plugins/plexum-openai-provider/config.json. // All fields optional. // // { // "binary": "codex", // "extra_args": ["--enable", "some-feature"], // "supported_models": ["codex","gpt-5"], // override manifest models list // "model_args": { // map plexum model id → --model value // "gpt-5": "gpt-5" // } // } type HostConfig struct { Binary string `json:"binary"` ExtraArgs []string `json:"extra_args"` SupportedModels []string `json:"supported_models"` ModelArgs map[string]string `json:"model_args"` } type openaiPlugin struct { host plugin.HostAPI cfg HostConfig // modelsAdvertised is the final list used for both the manifest // AND admission ("does the agent's model match one we serve?"). modelsAdvertised []string } func (p *openaiPlugin) Manifest() plugin.Manifest { // Manifest is built dynamically per init so an operator's // supported_models override surfaces correctly. loaded := p.loadConfigBareForManifest() models := loaded.SupportedModels if len(models) == 0 { models = defaultModels } return plugin.Manifest{ Name: pluginName, Version: "0.1.0", Activation: plugin.ActivationLazy, Executable: "plexum-openai-provider-plugin", Contracts: plugin.Contracts{ Provider: &plugin.ProviderContract{Models: models}, }, } } // loadConfigBareForManifest reads the config file once during // Manifest() (before Init runs) so the advertised model list reflects // operator overrides without requiring a second restart cycle. Errors // are silent — Init will surface them. func (p *openaiPlugin) loadConfigBareForManifest() HostConfig { profileRoot := os.Getenv("PLEXUM_PROFILE_ROOT") if profileRoot == "" { home, _ := os.UserHomeDir() profileRoot = filepath.Join(home, ".plexum") } raw, _ := os.ReadFile(filepath.Join(profileRoot, "plugins", pluginName, "config.json")) var c HostConfig _ = json.Unmarshal(raw, &c) return c } func (p *openaiPlugin) 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 = "codex" } if len(p.cfg.SupportedModels) > 0 { p.modelsAdvertised = p.cfg.SupportedModels } else { p.modelsAdvertised = defaultModels } host.Log("info", "openai (codex) provider initialized", map[string]any{ "binary": p.cfg.Binary, "extra_args": p.cfg.ExtraArgs, "models": p.modelsAdvertised, }) return nil } // Stream is the bare ProviderPlugin entrypoint; SDK uses // StreamWithAgent below when AgentContext is needed. func (p *openaiPlugin) Stream(ctx context.Context, modelID string, req canonical.TurnRequest) (<-chan canonical.TurnEvent, error) { return p.StreamWithAgent(ctx, modelID, plugin.AgentContext{}, req) } func (p *openaiPlugin) StreamWithAgent(ctx context.Context, modelID string, agent plugin.AgentContext, req canonical.TurnRequest) (<-chan canonical.TurnEvent, error) { cliModel := p.mapModel(modelID) return runner.Run(ctx, p.host, agent, cliModel, p.cfg.Binary, p.cfg.ExtraArgs, req) } // MutateSession is the session-mirror hook (decision #29x). codex // stores transcripts at ~/.codex/sessions///
/rollout-* // -.jsonl as response_item lines whose payloads carry // function_call / function_call_output entries keyed by `call_id` // (not toolu_*) — so a Plexum tool_use_id like "toolu_X" won't ever // match in there. // // v1 ships as a logged no-op: we resolve the file path so operator // log can confirm wiring, but we don't rewrite. When Plexum's tool // dispatch eventually routes through codex's native tool surface // (preserving call_id round-trip), the rewrite logic plugs in here. func (p *openaiPlugin) MutateSession(ctx context.Context, req plugin.SessionMutateRequest) error { path, err := runner.FindCodexSessionFile(req.Workspace) if err != nil { p.host.Log("debug", "codex session mutate: find failed", map[string]any{ "agent": req.AgentID, "err": err.Error(), }) return nil } p.host.Log("info", "codex session mutate (no-op v1)", map[string]any{ "agent": req.AgentID, "session_path": path, "requested": len(req.Mutations), "reason": "codex uses call_* ids; tool-call round-trip not yet wired", }) return nil } // mapModel resolves the Plexum-advertised model id into the CLI's // --model value. "" / "codex" → no --model flag (CLI default, only // thing ChatGPT-account installs support). func (p *openaiPlugin) mapModel(plexumModel string) string { if plexumModel == "" || plexumModel == "codex" { return "" } if v, ok := p.cfg.ModelArgs[plexumModel]; ok { return v } // Unknown id — pass through verbatim. Codex will accept or 400. return plexumModel } func main() { if err := plugin.Serve(&openaiPlugin{}); err != nil { fmt.Fprintf(os.Stderr, "plexum-openai-provider-plugin: %v\n", err) os.Exit(1) } }