156 lines
4.7 KiB
Go
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
|
|
}
|