* fix(i18n): clarify Go regex and field passthrough copy * feat(channel): support Codex upstream model discovery * Revert "fix(i18n): clarify Go regex and field passthrough copy" This reverts commit d63d7975db3e34ff44e189112d3c15ad8c24ad88.
181 lines
4.6 KiB
Go
181 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
)
|
|
|
|
const (
|
|
codexLatestReleaseURL = "https://api.github.com/repos/openai/codex/releases/latest"
|
|
codexClientVersionCacheTTL = time.Hour
|
|
)
|
|
|
|
type codexClientVersionCache struct {
|
|
sync.Mutex
|
|
version string
|
|
expiresAt time.Time
|
|
}
|
|
|
|
var latestCodexClientVersion codexClientVersionCache
|
|
|
|
func GetLatestCodexClientVersion(ctx context.Context, client *http.Client) (string, error) {
|
|
return latestCodexClientVersion.get(ctx, client, codexLatestReleaseURL, time.Now())
|
|
}
|
|
|
|
func (cache *codexClientVersionCache) get(ctx context.Context, client *http.Client, releaseURL string, now time.Time) (string, error) {
|
|
cache.Lock()
|
|
defer cache.Unlock()
|
|
|
|
if cache.version != "" && now.Before(cache.expiresAt) {
|
|
return cache.version, nil
|
|
}
|
|
|
|
version, err := fetchLatestCodexClientVersion(ctx, client, releaseURL)
|
|
if err != nil {
|
|
if cache.version != "" {
|
|
cache.expiresAt = now.Add(codexClientVersionCacheTTL)
|
|
return cache.version, nil
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
cache.version = version
|
|
cache.expiresAt = now.Add(codexClientVersionCacheTTL)
|
|
return version, nil
|
|
}
|
|
|
|
func fetchLatestCodexClientVersion(ctx context.Context, client *http.Client, releaseURL string) (string, error) {
|
|
if client == nil {
|
|
return "", fmt.Errorf("nil http client")
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releaseURL, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
req.Header.Set("User-Agent", "new-api")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return "", fmt.Errorf("codex release lookup failed: status=%d", resp.StatusCode)
|
|
}
|
|
|
|
var release struct {
|
|
Name string `json:"name"`
|
|
Draft bool `json:"draft"`
|
|
Prerelease bool `json:"prerelease"`
|
|
}
|
|
if err := common.DecodeJson(resp.Body, &release); err != nil {
|
|
return "", err
|
|
}
|
|
if release.Draft || release.Prerelease {
|
|
return "", fmt.Errorf("latest codex release is not stable")
|
|
}
|
|
version := strings.TrimSpace(release.Name)
|
|
if version == "" {
|
|
return "", fmt.Errorf("latest codex release has no version name")
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
func FetchCodexModels(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
baseURL string,
|
|
oauthKey *CodexOAuthKey,
|
|
clientVersion string,
|
|
) (statusCode int, models []string, err error) {
|
|
if client == nil {
|
|
return 0, nil, fmt.Errorf("nil http client")
|
|
}
|
|
if oauthKey == nil {
|
|
return 0, nil, fmt.Errorf("nil oauth key")
|
|
}
|
|
|
|
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
|
accessToken := strings.TrimSpace(oauthKey.AccessToken)
|
|
accountID := strings.TrimSpace(oauthKey.AccountID)
|
|
clientVersion = strings.TrimSpace(clientVersion)
|
|
if baseURL == "" {
|
|
return 0, nil, fmt.Errorf("empty baseURL")
|
|
}
|
|
if accessToken == "" {
|
|
return 0, nil, fmt.Errorf("codex channel: access_token is required")
|
|
}
|
|
if accountID == "" {
|
|
return 0, nil, fmt.Errorf("codex channel: account_id is required")
|
|
}
|
|
if clientVersion == "" {
|
|
return 0, nil, fmt.Errorf("codex channel: client_version is required")
|
|
}
|
|
|
|
modelsURL, err := url.Parse(baseURL + "/backend-api/codex/models")
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
query := modelsURL.Query()
|
|
query.Set("client_version", clientVersion)
|
|
modelsURL.RawQuery = query.Encode()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, modelsURL.String(), nil)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
req.Header.Set("ChatGPT-Account-Id", accountID)
|
|
req.Header.Set("User-Agent", "codex-cli/"+clientVersion)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return resp.StatusCode, nil, err
|
|
}
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return resp.StatusCode, nil, nil
|
|
}
|
|
|
|
var result struct {
|
|
Models []struct {
|
|
Slug string `json:"slug"`
|
|
} `json:"models"`
|
|
}
|
|
if err := common.Unmarshal(body, &result); err != nil {
|
|
return resp.StatusCode, nil, err
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(result.Models))
|
|
models = make([]string, 0, len(result.Models))
|
|
for _, item := range result.Models {
|
|
slug := strings.TrimSpace(item.Slug)
|
|
if slug == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[slug]; ok {
|
|
continue
|
|
}
|
|
seen[slug] = struct{}{}
|
|
models = append(models, slug)
|
|
}
|
|
return resp.StatusCode, models, nil
|
|
}
|