Files
aego/app/agentui/backend/codex_models.go
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

152 lines
4.1 KiB
Go

package backend
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
// CodexModel is an entry advertised by the installed Codex CLI.
type CodexModel struct {
ID, Label, Description string
Efforts []string
}
// CodexModels asks the CLI for its current model catalog without starting a
// conversation or changing its configuration. It does not create an MCP bridge.
func CodexModels(parent context.Context, command, dir string) ([]CodexModel, error) {
ctx, cancel := context.WithTimeout(parent, 15*time.Second)
defer cancel()
return codexModels(ctx, command, dir)
}
func codexModels(parent context.Context, command, dir string) ([]CodexModel, error) {
ctx, cancel := context.WithCancel(parent)
defer cancel()
cmd := exec.CommandContext(ctx, codexBinary(Config{Command: command}), codexArgs()...)
cmd.Dir = dir
hideCatalogProcess(cmd)
// The npm launcher forwards interrupts to the actual CLI. A bounded wait
// also handles launchers that ignore the signal or keep stdout open.
cmd.Cancel = func() error {
if err := cmd.Process.Signal(os.Interrupt); err != nil {
return cmd.Process.Kill()
}
return nil
}
cmd.WaitDelay = 2 * time.Second
in, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
out, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
cmd.Stderr = &codexStderr{}
if err = cmd.Start(); err != nil {
return nil, err
}
go func() {
<-ctx.Done()
_ = out.Close()
}()
defer func() {
cancel()
_ = in.Close()
_ = cmd.Wait()
}()
encoder := json.NewEncoder(in)
scanner := bufio.NewScanner(out)
scanner.Buffer(make([]byte, 4096), 4<<20)
request := func(id int, method string, params any, result any) error {
if err := encoder.Encode(map[string]any{"id": id, "method": method, "params": params}); err != nil {
return err
}
for scanner.Scan() {
var response codexEnvelope
if err := json.Unmarshal(scanner.Bytes(), &response); err != nil {
return err
}
if string(response.ID) != strconv.Itoa(id) {
continue // Notifications are not replies to this request.
}
if response.Error != nil {
return fmt.Errorf("model catalog: %s", response.Error.Message)
}
return json.Unmarshal(response.Result, result)
}
if ctx.Err() != nil {
return ctx.Err()
}
if err := scanner.Err(); err != nil {
return err
}
return errors.New("Codex closed the model catalog")
}
var initialized json.RawMessage
if err := request(1, "initialize", map[string]any{"clientInfo": map[string]string{"name": "aego_editor", "version": "1"}}, &initialized); err != nil {
return nil, err
}
if err := encoder.Encode(map[string]any{"method": "initialized", "params": map[string]any{}}); err != nil {
return nil, err
}
var models []CodexModel
seen := map[string]bool{}
cursor := ""
for page := 0; page < 16; page++ {
params := map[string]any{"limit": 100, "includeHidden": false}
if cursor != "" {
params["cursor"] = cursor
}
var result struct {
Data []struct {
Model string `json:"model"`
ID string `json:"id"`
Label string `json:"displayName"`
Description string `json:"description"`
Hidden bool `json:"hidden"`
Efforts []struct {
Value string `json:"reasoningEffort"`
} `json:"supportedReasoningEfforts"`
} `json:"data"`
Next string `json:"nextCursor"`
}
if err := request(page+2, "model/list", params, &result); err != nil {
return nil, err
}
for _, m := range result.Data {
id := strings.TrimSpace(m.Model)
if id == "" {
id = strings.TrimSpace(m.ID)
}
if id == "" || m.Hidden || seen[id] {
continue
}
seen[id] = true
entry := CodexModel{ID: id, Label: m.Label, Description: m.Description}
for _, effort := range m.Efforts {
if effort.Value != "" {
entry.Efforts = append(entry.Efforts, effort.Value)
}
}
models = append(models, entry)
}
if result.Next == "" {
return models, nil
}
if result.Next == cursor {
return nil, errors.New("Codex model catalog repeated a page")
}
cursor = result.Next
}
return nil, errors.New("Codex model catalog has too many pages")
}