137 lines
4.0 KiB
Go
137 lines
4.0 KiB
Go
package codex
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// ensureCodexProviderConfig writes or updates a [model_providers.<name>] section
|
|
// in $CODEX_HOME/config.toml so that Codex CLI can use the provider's wire_api
|
|
// and http_headers settings.
|
|
func ensureCodexProviderConfig(codexHome, name, baseURL, wireAPI string, headers map[string]string) error {
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
home, err := resolveCodexHomeForConfig(codexHome)
|
|
if err != nil {
|
|
return fmt.Errorf("codex: resolve codex home: %w", err)
|
|
}
|
|
if err := os.MkdirAll(home, 0o755); err != nil {
|
|
return fmt.Errorf("codex: mkdir codex home: %w", err)
|
|
}
|
|
|
|
cfgPath := filepath.Join(home, "config.toml")
|
|
raw, _ := os.ReadFile(cfgPath)
|
|
content := string(raw)
|
|
|
|
section := buildProviderSection(name, baseURL, wireAPI, headers)
|
|
updated := upsertProviderSection(content, name, section)
|
|
|
|
if err := os.WriteFile(cfgPath, []byte(updated), 0o644); err != nil {
|
|
return fmt.Errorf("codex: write config.toml: %w", err)
|
|
}
|
|
slog.Debug("codex: wrote provider config", "provider", name, "path", cfgPath)
|
|
return nil
|
|
}
|
|
|
|
// ensureCodexAuth writes $CODEX_HOME/auth.json with the provider's API key,
|
|
// matching cc-switch's approach: {"OPENAI_API_KEY": "...", "auth_mode": "api_key"}.
|
|
// This is the standard way to authenticate Codex CLI with third-party providers.
|
|
func ensureCodexAuth(codexHome, apiKey string) error {
|
|
if apiKey == "" {
|
|
return nil
|
|
}
|
|
home, err := resolveCodexHomeForConfig(codexHome)
|
|
if err != nil {
|
|
return fmt.Errorf("codex: resolve codex home: %w", err)
|
|
}
|
|
if err := os.MkdirAll(home, 0o755); err != nil {
|
|
return fmt.Errorf("codex: mkdir codex home: %w", err)
|
|
}
|
|
|
|
authPath := filepath.Join(home, "auth.json")
|
|
payload := map[string]any{
|
|
"OPENAI_API_KEY": apiKey,
|
|
"auth_mode": "apikey",
|
|
}
|
|
data, err := json.MarshalIndent(payload, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("codex: marshal auth.json: %w", err)
|
|
}
|
|
if err := os.WriteFile(authPath, append(data, '\n'), 0o600); err != nil {
|
|
return fmt.Errorf("codex: write auth.json: %w", err)
|
|
}
|
|
slog.Debug("codex: wrote auth.json", "path", authPath)
|
|
return nil
|
|
}
|
|
|
|
func resolveCodexHomeForConfig(explicit string) (string, error) {
|
|
if h := strings.TrimSpace(explicit); h != "" {
|
|
return h, nil
|
|
}
|
|
if h := strings.TrimSpace(os.Getenv("CODEX_HOME")); h != "" {
|
|
return h, nil
|
|
}
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(homeDir, ".codex"), nil
|
|
}
|
|
|
|
func buildProviderSection(name, baseURL, wireAPI string, headers map[string]string) string {
|
|
var sb strings.Builder
|
|
fmt.Fprintf(&sb, "[model_providers.%s]\n", name)
|
|
fmt.Fprintf(&sb, "name = %q\n", name)
|
|
if baseURL != "" {
|
|
fmt.Fprintf(&sb, "base_url = %q\n", baseURL)
|
|
}
|
|
fmt.Fprintf(&sb, "env_key = %q\n", "OPENAI_API_KEY")
|
|
if wireAPI != "" {
|
|
fmt.Fprintf(&sb, "wire_api = %q\n", wireAPI)
|
|
}
|
|
if len(headers) > 0 {
|
|
fmt.Fprintf(&sb, "\n[model_providers.%s.http_headers]\n", name)
|
|
for k, v := range headers {
|
|
fmt.Fprintf(&sb, "%q = %q\n", k, v)
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// upsertProviderSection replaces an existing [model_providers.<name>] section
|
|
// or appends a new one at the end of the config content.
|
|
func upsertProviderSection(content, name, newSection string) string {
|
|
sectionHeader := fmt.Sprintf("[model_providers.%s]", name)
|
|
subSectionPrefix := fmt.Sprintf("[model_providers.%s.", name)
|
|
|
|
if !strings.Contains(content, sectionHeader) {
|
|
trimmed := strings.TrimRight(content, "\n\t ")
|
|
if trimmed == "" {
|
|
return newSection
|
|
}
|
|
return trimmed + "\n\n" + newSection
|
|
}
|
|
|
|
idx := strings.Index(content, sectionHeader)
|
|
|
|
after := content[idx+len(sectionHeader):]
|
|
end := len(content)
|
|
lines := strings.Split(after, "\n")
|
|
offset := idx + len(sectionHeader)
|
|
for _, line := range lines {
|
|
offset += len(line) + 1
|
|
trimmed := strings.TrimSpace(line)
|
|
if len(trimmed) > 0 && trimmed[0] == '[' && !strings.HasPrefix(trimmed, subSectionPrefix) && trimmed != sectionHeader {
|
|
end = offset - len(line) - 1
|
|
break
|
|
}
|
|
}
|
|
|
|
return strings.TrimRight(content[:idx], "\n") + "\n\n" + newSection + "\n" + content[end:]
|
|
}
|