package codex import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "net" "net/http" "os" "sort" "strings" "time" ) const CanvasInputLimit = 1 << 20 type CanvasModel struct { ID string `json:"id"` Label string `json:"label"` Provider string `json:"provider"` InputModalities []string `json:"input_modalities"` OutputModalities []string `json:"output_modalities"` Available bool `json:"available"` MaxExecutionMS int `json:"max_execution_ms"` MaxInputBytes int `json:"max_input_bytes"` ParametersSchema map[string]any `json:"parameters_schema"` Backend string `json:"-"` } // Only curated catalog fields leave the worker. Raw CLI cache metadata, // local paths, authentication files and backend configuration stay private. func workerCanvasModels() ([]CanvasModel, error) { raw, err := os.ReadFile("/home/node/.codex/models_cache.json") if err != nil { return nil, err } var cache struct { Models []struct { Slug string `json:"slug"` Label string `json:"display_name"` Visibility string `json:"visibility"` Inputs []string `json:"input_modalities"` } `json:"models"` } if err = json.Unmarshal(raw, &cache); err != nil { return nil, err } result := []CanvasModel{} for _, entry := range cache.Models { if entry.Visibility != "list" || entry.Slug == "" || strings.HasPrefix(entry.Slug, "-") || len(entry.Slug) > 128 { continue } sum := sha256.Sum256([]byte("codex-cli/" + entry.Slug)) inputs := []string{} for _, value := range entry.Inputs { if value == "text" || value == "image" { inputs = append(inputs, value) } } if len(inputs) == 0 { continue } label := entry.Label if label == "" { label = entry.Slug } if len(label) > 128 { label = entry.Slug } result = append(result, CanvasModel{ID: "codex-" + hex.EncodeToString(sum[:16]), Label: label, Provider: "codex-cli", InputModalities: inputs, OutputModalities: []string{"text"}, Available: true, MaxExecutionMS: 90000, MaxInputBytes: CanvasInputLimit, ParametersSchema: map[string]any{"type": "object", "properties": map[string]any{}, "additionalProperties": false}, Backend: entry.Slug}) } verified := map[string]bool{} for _, id := range strings.Split(os.Getenv("FOUNDRY_CANVAS_IMAGE_MODELS"), ",") { id = strings.TrimSpace(id) if id != "" { verified[id] = true } } textModels := append([]CanvasModel{}, result...) for _, model := range textModels { if !verified[model.Backend] { continue } sum := sha256.Sum256([]byte("codex-cli/" + model.Backend + "/image")) model.ID = "codex-" + hex.EncodeToString(sum[:16]) model.Label += " ยท Image" model.OutputModalities = []string{"image"} model.MaxExecutionMS = 180000 result = append(result, model) } sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID }) return result, nil } type CanvasClient struct{ client *http.Client } func NewCanvasClient(socket string) *CanvasClient { if socket == "" { return nil } transport := &http.Transport{DisableKeepAlives: true, DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, "unix", socket) }} return &CanvasClient{client: &http.Client{Transport: transport, Timeout: 182 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}} } func (c *CanvasClient) Models(ctx context.Context) ([]CanvasModel, error) { if c == nil { return nil, errors.New("codex_unavailable") } request, err := http.NewRequestWithContext(ctx, "GET", "http://codex/canvas/models", nil) if err != nil { return nil, err } response, err := c.client.Do(request) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != 200 { return nil, errors.New("codex_unavailable") } raw, err := io.ReadAll(io.LimitReader(response.Body, 256<<10)) if err != nil { return nil, err } var result struct { Models []CanvasModel `json:"models"` } if err = json.Unmarshal(raw, &result); err != nil { return nil, err } return result.Models, nil } type CanvasTask struct { OutputModality string `json:"output_modality,omitempty"` Images []CanvasImage `json:"images,omitempty"` ModelID string `json:"model_id"` Prompt string `json:"prompt"` Parameters map[string]any `json:"parameters"` } func (c *CanvasClient) Generate(ctx context.Context, input CanvasTask) (string, error) { output, err := c.GenerateOutput(ctx, input) if err != nil { return "", err } if output.Kind != "text" { return "", errors.New("model_incompatible") } return output.Text, nil }