Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
216 lines
6.7 KiB
Go
216 lines
6.7 KiB
Go
package modelcatalog
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// ParseCodexModelListLine parses one JSON-RPC stdout line. handled is false
|
|
// for notifications, logs, and responses to other request ids.
|
|
func ParseCodexModelListLine(line []byte, requestID string) ([]ModelOption, bool, error) {
|
|
var payload map[string]json.RawMessage
|
|
if err := json.Unmarshal(line, &payload); err != nil {
|
|
return nil, false, nil
|
|
}
|
|
if !codexRPCIDMatches(payload["id"], requestID) {
|
|
return nil, false, nil
|
|
}
|
|
if rawError, ok := payload["error"]; ok && string(rawError) != "null" {
|
|
return nil, true, errors.New(extractCodexRPCError(rawError))
|
|
}
|
|
var result struct {
|
|
Data []json.RawMessage `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(payload["result"], &result); err != nil {
|
|
return nil, true, fmt.Errorf("parse codex model/list result: %w", err)
|
|
}
|
|
models := make([]ModelOption, 0, len(result.Data))
|
|
for _, rawModel := range result.Data {
|
|
if model, ok := NormalizeCodexModel(rawModel); ok {
|
|
models = append(models, model)
|
|
}
|
|
}
|
|
return models, true, nil
|
|
}
|
|
|
|
func NormalizeCodexModel(raw json.RawMessage) (ModelOption, bool) {
|
|
var object map[string]any
|
|
if err := json.Unmarshal(raw, &object); err != nil {
|
|
return ModelOption{}, false
|
|
}
|
|
id := firstNonBlank(stringMapValue(object, "model"), stringMapValue(object, "id"))
|
|
if id == "" {
|
|
return ModelOption{}, false
|
|
}
|
|
displayName := firstNonBlank(stringMapValue(object, "displayName"), stringMapValue(object, "display_name"), id)
|
|
reasoningValue, reasoningAdvertised := advertisedValue(object, "supportedReasoningEfforts", "supported_reasoning_efforts")
|
|
speedValue, speedAdvertised := advertisedValue(object, "serviceTiers", "service_tiers")
|
|
legacySpeedValue, legacySpeedAdvertised := advertisedValue(object, "additionalSpeedTiers", "additional_speed_tiers")
|
|
speeds := normalizeCodexSpeeds(speedValue, legacySpeedValue)
|
|
defaultSpeed := canonicalCodexSpeed(firstNonBlank(stringMapValue(object, "defaultServiceTier"), stringMapValue(object, "default_service_tier")))
|
|
defaultSpeed = validSpeedDefault(speeds, defaultSpeed)
|
|
return ModelOption{
|
|
ID: id,
|
|
DisplayName: displayName,
|
|
Description: stringMapValue(object, "description"),
|
|
DefaultReasoningEffort: firstNonBlank(stringMapValue(object, "defaultReasoningEffort"), stringMapValue(object, "default_reasoning_effort")),
|
|
DefaultSpeed: defaultSpeed,
|
|
IsDefault: boolMapValue(object, "isDefault") || boolMapValue(object, "is_default"),
|
|
ReasoningEffortsAdvertised: reasoningAdvertised,
|
|
SupportedReasoningEfforts: normalizeCodexReasoningEfforts(reasoningValue),
|
|
SpeedsAdvertised: speedAdvertised || legacySpeedAdvertised,
|
|
SupportedSpeeds: speeds,
|
|
SupportsImageInput: codexImageInputSupport(object),
|
|
}, true
|
|
}
|
|
|
|
func normalizeCodexReasoningEfforts(value any) []ReasoningEffortOption {
|
|
rawOptions, ok := value.([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
options := make([]ReasoningEffortOption, 0, len(rawOptions))
|
|
seen := make(map[string]struct{}, len(rawOptions))
|
|
for _, rawOption := range rawOptions {
|
|
var option ReasoningEffortOption
|
|
switch typed := rawOption.(type) {
|
|
case string:
|
|
option.Value = strings.TrimSpace(typed)
|
|
case map[string]any:
|
|
option.Value = firstNonBlank(stringMapValue(typed, "reasoningEffort"), stringMapValue(typed, "effort"), stringMapValue(typed, "value"))
|
|
option.Label = firstNonBlank(stringMapValue(typed, "label"), stringMapValue(typed, "name"))
|
|
option.Description = stringMapValue(typed, "description")
|
|
option.Default = boolMapValue(typed, "default") || boolMapValue(typed, "isDefault")
|
|
}
|
|
if option.Value == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[option.Value]; exists {
|
|
continue
|
|
}
|
|
seen[option.Value] = struct{}{}
|
|
options = append(options, option)
|
|
}
|
|
return options
|
|
}
|
|
|
|
func normalizeCodexSpeeds(values ...any) []SpeedOption {
|
|
options := []SpeedOption{{Value: "standard", Label: "Standard"}}
|
|
seen := map[string]struct{}{"standard": {}}
|
|
for _, rawValue := range values {
|
|
rawOptions, ok := rawValue.([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, rawOption := range rawOptions {
|
|
var option SpeedOption
|
|
switch typed := rawOption.(type) {
|
|
case string:
|
|
option.Value = canonicalCodexSpeed(typed)
|
|
case map[string]any:
|
|
option.Value = canonicalCodexSpeed(stringMapValue(typed, "id"))
|
|
option.Label = stringMapValue(typed, "name")
|
|
option.Description = stringMapValue(typed, "description")
|
|
}
|
|
if option.Value == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[option.Value]; exists {
|
|
continue
|
|
}
|
|
seen[option.Value] = struct{}{}
|
|
options = append(options, option)
|
|
}
|
|
}
|
|
return options
|
|
}
|
|
|
|
func canonicalCodexSpeed(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "", "default", "standard":
|
|
return "standard"
|
|
case "priority", "fast":
|
|
return "fast"
|
|
default:
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
|
|
func codexImageInputSupport(object map[string]any) *bool {
|
|
raw, advertised := object["inputModalities"]
|
|
if !advertised {
|
|
raw, advertised = object["input_modalities"]
|
|
}
|
|
if !advertised {
|
|
return nil
|
|
}
|
|
modalities, ok := raw.([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
supported := false
|
|
for _, modality := range modalities {
|
|
if value, ok := modality.(string); ok && strings.EqualFold(strings.TrimSpace(value), "image") {
|
|
supported = true
|
|
break
|
|
}
|
|
}
|
|
return &supported
|
|
}
|
|
|
|
func advertisedValue(object map[string]any, keys ...string) (any, bool) {
|
|
for _, key := range keys {
|
|
if value, ok := object[key]; ok {
|
|
return value, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func codexRPCIDMatches(raw json.RawMessage, want string) bool {
|
|
var stringID string
|
|
if err := json.Unmarshal(raw, &stringID); err == nil {
|
|
return stringID == want
|
|
}
|
|
var numberID int
|
|
if err := json.Unmarshal(raw, &numberID); err == nil {
|
|
return fmt.Sprintf("%d", numberID) == want
|
|
}
|
|
return false
|
|
}
|
|
|
|
func extractCodexRPCError(raw json.RawMessage) string {
|
|
var message string
|
|
if err := json.Unmarshal(raw, &message); err == nil && strings.TrimSpace(message) != "" {
|
|
return strings.TrimSpace(message)
|
|
}
|
|
var object struct {
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal(raw, &object); err == nil && strings.TrimSpace(object.Message) != "" {
|
|
return strings.TrimSpace(object.Message)
|
|
}
|
|
return "unknown codex app-server RPC error"
|
|
}
|
|
|
|
func stringMapValue(object map[string]any, key string) string {
|
|
value, _ := object[key].(string)
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func boolMapValue(object map[string]any, key string) bool {
|
|
value, ok := object[key].(bool)
|
|
return ok && value
|
|
}
|
|
|
|
func firstNonBlank(values ...string) string {
|
|
for _, value := range values {
|
|
if value = strings.TrimSpace(value); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|