Files
codex-bot d6ee8942f1
Foundry Go checks / check (push) Successful in 2m40s
Foundry Go checks / check (pull_request) Successful in 2m36s
Implement Canvas V2 subgraphs, assets and asynchronous AI execution
2026-11-06 18:36:00 +08:00

156 lines
4.7 KiB
Go

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
}