mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 14:41:21 +00:00
feat(channel): support Codex upstream model discovery (#6184)
* 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 d63d7975db.
This commit is contained in:
+10
-86
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/QuantumNous/new-api/i18n"
|
"github.com/QuantumNous/new-api/i18n"
|
||||||
"github.com/QuantumNous/new-api/model"
|
"github.com/QuantumNous/new-api/model"
|
||||||
relaychannel "github.com/QuantumNous/new-api/relay/channel"
|
relaychannel "github.com/QuantumNous/new-api/relay/channel"
|
||||||
"github.com/QuantumNous/new-api/relay/channel/gemini"
|
|
||||||
"github.com/QuantumNous/new-api/relay/channel/ollama"
|
"github.com/QuantumNous/new-api/relay/channel/ollama"
|
||||||
"github.com/QuantumNous/new-api/service"
|
"github.com/QuantumNous/new-api/service"
|
||||||
"github.com/QuantumNous/new-api/service/authz"
|
"github.com/QuantumNous/new-api/service/authz"
|
||||||
@@ -1176,102 +1175,27 @@ func FetchModels(c *gin.Context) {
|
|||||||
baseURL = constant.ChannelBaseURLs[req.Type]
|
baseURL = constant.ChannelBaseURLs[req.Type]
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove line breaks and extra spaces.
|
|
||||||
key := strings.TrimSpace(req.Key)
|
key := strings.TrimSpace(req.Key)
|
||||||
key = strings.Split(key, "\n")[0]
|
if req.Type != constant.ChannelTypeCodex {
|
||||||
|
key = strings.Split(key, "\n")[0]
|
||||||
if req.Type == constant.ChannelTypeOllama {
|
|
||||||
models, err := ollama.FetchOllamaModels(baseURL, key)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": fmt.Sprintf("获取Ollama模型失败: %s", err.Error()),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
names := make([]string, 0, len(models))
|
|
||||||
for _, modelInfo := range models {
|
|
||||||
names = append(names, modelInfo.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
"data": names,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Type == constant.ChannelTypeGemini {
|
channel := &model.Channel{
|
||||||
models, err := gemini.FetchGeminiModels(baseURL, key, "")
|
Type: req.Type,
|
||||||
if err != nil {
|
Key: key,
|
||||||
c.JSON(http.StatusOK, gin.H{
|
BaseURL: &baseURL,
|
||||||
"success": false,
|
|
||||||
"message": fmt.Sprintf("获取Gemini模型失败: %s", err.Error()),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
"data": models,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
models, err := fetchChannelUpstreamModelIDs(channel)
|
||||||
client := &http.Client{}
|
|
||||||
url := fmt.Sprintf("%s/v1/models", baseURL)
|
|
||||||
|
|
||||||
request, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": err.Error(),
|
"message": fmt.Sprintf("获取模型列表失败: %s", err.Error()),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Header.Set("Authorization", "Bearer "+key)
|
|
||||||
|
|
||||||
response, err := client.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//check status code
|
|
||||||
if response.StatusCode != http.StatusOK {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": "Failed to fetch models",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
Data []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var models []string
|
|
||||||
for _, model := range result.Data {
|
|
||||||
models = append(models, model.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
|
"message": "",
|
||||||
"data": models,
|
"data": models,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,6 +285,10 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
|
|||||||
return normalizeModelNames(models), nil
|
return normalizeModelNames(models), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if channel.Type == constant.ChannelTypeCodex {
|
||||||
|
return service.FetchCodexChannelModels(channel)
|
||||||
|
}
|
||||||
|
|
||||||
var url string
|
var url string
|
||||||
switch channel.Type {
|
switch channel.Type {
|
||||||
case constant.ChannelTypeAli:
|
case constant.ChannelTypeAli:
|
||||||
|
|||||||
@@ -3,14 +3,51 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"github.com/QuantumNous/new-api/constant"
|
||||||
"github.com/QuantumNous/new-api/dto"
|
"github.com/QuantumNous/new-api/dto"
|
||||||
"github.com/QuantumNous/new-api/model"
|
"github.com/QuantumNous/new-api/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestFetchModelsUsesSharedChannelFetchBehavior(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/v1/models" {
|
||||||
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
if r.Header.Get("x-api-key") != "first-key" {
|
||||||
|
t.Errorf("unexpected x-api-key header: %s", r.Header.Get("x-api-key"))
|
||||||
|
}
|
||||||
|
if r.Header.Get("Authorization") != "" {
|
||||||
|
t.Errorf("unexpected Authorization header: %s", r.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"data":[{"id":" claude-sonnet "},{"id":"claude-sonnet"}]}`))
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
body, err := common.Marshal(map[string]any{
|
||||||
|
"base_url": server.URL,
|
||||||
|
"type": constant.ChannelTypeAnthropic,
|
||||||
|
"key": "first-key\nsecond-key",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/channel/fetch_models", strings.NewReader(string(body)))
|
||||||
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
FetchModels(ctx)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
require.JSONEq(t, `{"success":true,"message":"","data":["claude-sonnet"]}`, recorder.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeModelNames(t *testing.T) {
|
func TestNormalizeModelNames(t *testing.T) {
|
||||||
result := normalizeModelNames([]string{
|
result := normalizeModelNames([]string{
|
||||||
" gpt-4o ",
|
" gpt-4o ",
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
package codex
|
package codex
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||||
"github.com/samber/lo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var baseModelList = []string{
|
var baseModelList = []string{
|
||||||
"gpt-5", "gpt-5-codex", "gpt-5-codex-mini",
|
"gpt-5.6-sol",
|
||||||
"gpt-5.1", "gpt-5.1-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini",
|
"gpt-5.6-terra",
|
||||||
"gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark",
|
"gpt-5.6-luna",
|
||||||
"gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
|
"gpt-5.5",
|
||||||
|
"gpt-5.4",
|
||||||
|
"gpt-5.4-mini",
|
||||||
|
"gpt-5.3-codex-spark",
|
||||||
|
"codex-auto-review",
|
||||||
}
|
}
|
||||||
|
|
||||||
var ModelList = withCompactModelSuffix(baseModelList)
|
var ModelList = slices.DeleteFunc(
|
||||||
|
ratio_setting.WithCompactModelVariants(baseModelList),
|
||||||
|
func(modelName string) bool {
|
||||||
|
return modelName == ratio_setting.WithCompactModelSuffix("codex-auto-review")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const ChannelName = "codex"
|
const ChannelName = "codex"
|
||||||
|
|
||||||
func withCompactModelSuffix(models []string) []string {
|
|
||||||
out := make([]string, 0, len(models)*2)
|
|
||||||
out = append(out, models...)
|
|
||||||
out = append(out, lo.Map(models, func(model string, _ int) string {
|
|
||||||
return ratio_setting.WithCompactModelSuffix(model)
|
|
||||||
})...)
|
|
||||||
return lo.Uniq(out)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/constant"
|
||||||
|
"github.com/QuantumNous/new-api/model"
|
||||||
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FetchCodexChannelModels(channel *model.Channel) ([]string, error) {
|
||||||
|
if channel == nil || channel.Type != constant.ChannelTypeCodex {
|
||||||
|
return nil, fmt.Errorf("channel type is not Codex")
|
||||||
|
}
|
||||||
|
if channel.ChannelInfo.IsMultiKey {
|
||||||
|
return nil, fmt.Errorf("codex channel does not support multi-key model discovery")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewProxyHttpClient(channel.GetSetting().Proxy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
clientVersion, err := GetLatestCodexClientVersion(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get Codex client version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := channel.GetBaseURL()
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = constant.ChannelBaseURLs[constant.ChannelTypeCodex]
|
||||||
|
}
|
||||||
|
return fetchCodexChannelModels(ctx, channel, baseURL, client, clientVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCodexChannelModels(
|
||||||
|
ctx context.Context,
|
||||||
|
channel *model.Channel,
|
||||||
|
baseURL string,
|
||||||
|
client *http.Client,
|
||||||
|
clientVersion string,
|
||||||
|
) ([]string, error) {
|
||||||
|
oauthKey, err := parseCodexOAuthKey(strings.TrimSpace(channel.Key))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode, models, err := FetchCodexModels(ctx, client, baseURL, oauthKey, clientVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if statusCode == http.StatusUnauthorized {
|
||||||
|
if channel.Id <= 0 {
|
||||||
|
return nil, fmt.Errorf("codex channel credential expired; save the channel before retrying model fetch")
|
||||||
|
}
|
||||||
|
refreshedKey, _, refreshErr := RefreshCodexChannelCredential(
|
||||||
|
ctx,
|
||||||
|
channel.Id,
|
||||||
|
CodexCredentialRefreshOptions{ResetCaches: true},
|
||||||
|
)
|
||||||
|
if refreshErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to refresh Codex channel credential: %w", refreshErr)
|
||||||
|
}
|
||||||
|
statusCode, models, err = FetchCodexModels(ctx, client, baseURL, &CodexOAuthKey{
|
||||||
|
AccessToken: refreshedKey.AccessToken,
|
||||||
|
AccountID: refreshedKey.AccountID,
|
||||||
|
}, clientVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
|
||||||
|
return nil, fmt.Errorf("upstream status: %d", statusCode)
|
||||||
|
}
|
||||||
|
modelVariants := make([]string, 0, len(models)*2)
|
||||||
|
modelVariants = append(modelVariants, models...)
|
||||||
|
for _, modelName := range models {
|
||||||
|
if modelName == "codex-auto-review" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
modelVariants = append(modelVariants, ratio_setting.WithCompactModelSuffix(modelName))
|
||||||
|
}
|
||||||
|
return modelVariants, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -11,3 +11,24 @@ func WithCompactModelSuffix(modelName string) string {
|
|||||||
}
|
}
|
||||||
return modelName + CompactModelSuffix
|
return modelName + CompactModelSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithCompactModelVariants(models []string) []string {
|
||||||
|
variants := make([]string, 0, len(models)*2)
|
||||||
|
seen := make(map[string]struct{}, len(models)*2)
|
||||||
|
for _, model := range models {
|
||||||
|
if _, ok := seen[model]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[model] = struct{}{}
|
||||||
|
variants = append(variants, model)
|
||||||
|
}
|
||||||
|
for _, model := range models {
|
||||||
|
compactModel := WithCompactModelSuffix(model)
|
||||||
|
if _, ok := seen[compactModel]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[compactModel] = struct{}{}
|
||||||
|
variants = append(variants, compactModel)
|
||||||
|
}
|
||||||
|
return variants
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -377,7 +377,7 @@ export const FIELD_DESCRIPTIONS = {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const MODEL_FETCHABLE_TYPES = new Set([
|
export const MODEL_FETCHABLE_TYPES = new Set([
|
||||||
1, 4, 14, 17, 20, 23, 24, 25, 26, 27, 31, 34, 35, 40, 42, 43, 47, 48,
|
1, 4, 14, 17, 20, 23, 24, 25, 26, 27, 31, 34, 35, 40, 42, 43, 47, 48, 57,
|
||||||
])
|
])
|
||||||
|
|
||||||
export const TYPE_TO_KEY_PROMPT: Record<number, string> = {
|
export const TYPE_TO_KEY_PROMPT: Record<number, string> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user