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
212 lines
5.1 KiB
Go
212 lines
5.1 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/tutti-os/tutti/packages/agent/daemon/providerregistry"
|
|
)
|
|
|
|
func providerRuntimeConfig(session Session, provider string) map[string]any {
|
|
baseURL := providerBaseURL(session, provider)
|
|
if baseURL == "" {
|
|
return nil
|
|
}
|
|
return map[string]any{
|
|
"baseUrl": baseURL,
|
|
}
|
|
}
|
|
|
|
func providerBaseURL(session Session, provider string) string {
|
|
env := append(os.Environ(), session.Env...)
|
|
if descriptor, ok := providerregistry.Find(provider); ok {
|
|
return providerBaseURLFromDescriptor(env, session.CWD, descriptor.Runtime.Endpoint)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func providerBaseURLFromDescriptor(
|
|
env []string,
|
|
cwd string,
|
|
endpoint providerregistry.RuntimeEndpointDescriptor,
|
|
) string {
|
|
if baseURL := firstNonEmptyEnv(env, endpoint.BaseURLEnvVars...); baseURL != "" {
|
|
return baseURL
|
|
}
|
|
switch endpoint.ConfigKind {
|
|
case providerregistry.EndpointConfigKindCodexCLI:
|
|
return codexConfigBaseURL(env)
|
|
case providerregistry.EndpointConfigKindClaudeSettings:
|
|
return claudeSettingsBaseURL(env, cwd)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func claudeSettingsBaseURL(env []string, cwd string) string {
|
|
var candidates []string
|
|
configDir := firstNonEmptyEnv(env, "CLAUDE_CONFIG_DIR")
|
|
if configDir == "" {
|
|
if home, err := os.UserHomeDir(); err == nil && strings.TrimSpace(home) != "" {
|
|
configDir = filepath.Join(home, ".claude")
|
|
}
|
|
}
|
|
if configDir != "" {
|
|
candidates = append(candidates, filepath.Join(configDir, "settings.json"))
|
|
}
|
|
candidates = append(candidates, claudeProjectSettingsPaths(cwd)...)
|
|
for index := len(candidates) - 1; index >= 0; index-- {
|
|
if baseURL := claudeSettingsEnvValue(candidates[index], "ANTHROPIC_BASE_URL"); baseURL != "" {
|
|
return baseURL
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func claudeProjectSettingsPaths(cwd string) []string {
|
|
cwd = strings.TrimSpace(cwd)
|
|
if cwd == "" {
|
|
return nil
|
|
}
|
|
var dirs []string
|
|
current := filepath.Clean(cwd)
|
|
for {
|
|
dirs = append(dirs, current)
|
|
parent := filepath.Dir(current)
|
|
if parent == current {
|
|
break
|
|
}
|
|
current = parent
|
|
}
|
|
var candidates []string
|
|
for index := len(dirs) - 1; index >= 0; index-- {
|
|
dir := dirs[index]
|
|
candidates = append(candidates,
|
|
filepath.Join(dir, ".claude", "settings.json"),
|
|
filepath.Join(dir, ".claude", "settings.local.json"),
|
|
)
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
func claudeSettingsEnvValue(path string, key string) string {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var parsed struct {
|
|
Env map[string]any `json:"env"`
|
|
}
|
|
if err := json.Unmarshal(content, &parsed); err != nil {
|
|
return ""
|
|
}
|
|
value, ok := parsed.Env[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
switch typed := value.(type) {
|
|
case string:
|
|
return strings.TrimSpace(typed)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func firstNonEmptyEnv(env []string, keys ...string) string {
|
|
for _, key := range keys {
|
|
if value := envValueLast(env, key); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func envValueLast(env []string, key string) string {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return ""
|
|
}
|
|
prefix := key + "="
|
|
for index := len(env) - 1; index >= 0; index-- {
|
|
entry := env[index]
|
|
if strings.HasPrefix(entry, prefix) {
|
|
return strings.TrimSpace(strings.TrimPrefix(entry, prefix))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func codexConfigBaseURL(env []string) string {
|
|
codexHome := envValueLast(env, "CODEX_HOME")
|
|
if codexHome == "" {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil || strings.TrimSpace(home) == "" {
|
|
return ""
|
|
}
|
|
codexHome = filepath.Join(home, ".codex")
|
|
}
|
|
content, err := os.ReadFile(filepath.Join(codexHome, "config.toml"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
modelProvider := ""
|
|
chatGPTBaseURL := ""
|
|
providerBaseURLs := map[string]string{}
|
|
currentProvider := ""
|
|
for _, rawLine := range strings.Split(string(content), "\n") {
|
|
line := strings.TrimSpace(strings.SplitN(rawLine, "#", 2)[0])
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
|
currentProvider = codexModelProviderSection(line)
|
|
continue
|
|
}
|
|
key, value, ok := splitSimpleTomlAssignment(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch {
|
|
case currentProvider != "" && key == "base_url":
|
|
providerBaseURLs[currentProvider] = value
|
|
case currentProvider == "" && key == "model_provider":
|
|
modelProvider = value
|
|
case currentProvider == "" && key == "chatgpt_base_url":
|
|
chatGPTBaseURL = value
|
|
}
|
|
}
|
|
if modelProvider != "" {
|
|
if baseURL := strings.TrimSpace(providerBaseURLs[modelProvider]); baseURL != "" {
|
|
return baseURL
|
|
}
|
|
}
|
|
if chatGPTBaseURL != "" {
|
|
return chatGPTBaseURL
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func codexModelProviderSection(line string) string {
|
|
section := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "["), "]"))
|
|
const prefix = "model_providers."
|
|
if !strings.HasPrefix(section, prefix) {
|
|
return ""
|
|
}
|
|
return strings.Trim(strings.TrimSpace(strings.TrimPrefix(section, prefix)), `"'`)
|
|
}
|
|
|
|
func splitSimpleTomlAssignment(line string) (string, string, bool) {
|
|
left, right, ok := strings.Cut(line, "=")
|
|
if !ok {
|
|
return "", "", false
|
|
}
|
|
key := strings.TrimSpace(left)
|
|
value := strings.Trim(strings.TrimSpace(right), `"'`)
|
|
if key == "" || value == "" {
|
|
return "", "", false
|
|
}
|
|
return key, value, true
|
|
}
|