diff --git a/.downstream/native-domains.md b/.downstream/native-domains.md index 9404eb576641..bcc457f4b1eb 100644 --- a/.downstream/native-domains.md +++ b/.downstream/native-domains.md @@ -123,12 +123,19 @@ source, proxy and profile. The Codex-only WebSocket adapter retains upstream coder/websocket v1.8.14 and its license, adding actual bounded deflate-window support; other services use the original dependency. -Version discovery still runs every six hours. Automatic effective identity advances -only within the shipped verified profile set; newly discovered unsupported releases -are visible as pending validation. Saving an unsupported manual version returns an -error. Settings reads add verified/pending/strict-profile details without changing -the four-field write payload. Existing device seeds and persisted hardware identities -are retained; their supported platform/architecture classes match the verified set. +Version discovery runs every six hours and directly advances the effective client +identity. Precedence is a normalized manual version, the discovered stable version, +then the built-in default (subject to the existing minimum version). Client releases +are not gated on transport sampling. The transport retains its recorded reference +version and source; `Binding.client_version` records the actual requested identity +and participates in pool keys. `Binding.version` and `validated` describe the sampled +transport reference, not validation of a later client release. Settings GET adds +`version_source` (`manual`, `synced`, `builtin`); `verified_version` retains the +reference version and the compatibility `version_pending_validation` is false. +The four-field settings write payload is unchanged. Existing device seeds and +persisted hardware identities are retained. The identity page shows the effective +and discovered versions with their source, with full UA/reference details available +in disclosures. No new sampling or per-client-release publishing pipeline is required. One immutable policy/identity selection is shared by each attempt's headers, body and transport. New requests freeze the compression choice too. WS pool compatibility @@ -139,11 +146,22 @@ handshake. Account mode edits invalidate only that account's subsequent connecti ## Independent Pelican tests -`/admin/pelican-tests` opens a paginated gallery of all observations from the last -24 hours, with task/account/model/effort/scene/execution/verdict filters. Wide -screens show four cards, medium screens two, and phones one. The new-task composer loads -accounts in pages of 50, retains selection across pages, and offers explicit -current-page/all-matching scopes. Models use locally saved mappings and catalogs; +`/admin/pelican-tests` opens a paginated gallery of observations from the last +24 hours with Works and Tasks views. Account name/ID search, model and verdict are +primary filters; effort, execution and legacy scene remain in More filters. Active +filters are removable. URL query keys retain the view (`gallery`, `tasks`, `new`), +selected `task`, `page`, `task_page` and existing gallery filters (`account_id`, +`model`, `effort`, `scene`, `verdict`, `status`). Four-to-three previews use one +column below 768px, two from 768px, three from 1280px and four from 1920px. +The single-page new-task composer shows account selection and model configuration +side by side on wide screens, with a sticky launch summary. It loads accounts in +pages of 50, retains cross-page selections and explicit current-page/all-matching +scopes. Selected account groups are paginated in sets of 20 without changing the +submitted selection. Per-user session drafts retain configuration on navigation or +rejected submissions and are cleared only on accepted creation or explicit clear. +Task cards expose progress and open the task gallery; the comparison tray remains +at the bottom. Work details group the preview, summary, results and complete raw +evidence; comparisons collapse detailed evidence by default. Models use locally saved mappings and catalogs; searchable dropdowns accept catalog choices only. Backend defaults prefer `gpt-6.1-sol` for OpenAI and `claude-opus-5-5` for Anthropic, falling back to the first text-capable local choice. Effort defaults to the model default. diff --git a/backend/internal/pkg/codextransport/client_version_test.go b/backend/internal/pkg/codextransport/client_version_test.go new file mode 100644 index 000000000000..5692f43a857b --- /dev/null +++ b/backend/internal/pkg/codextransport/client_version_test.go @@ -0,0 +1,25 @@ +package codextransport + +import "testing" + +func TestCodexTransportClientVersionIndependentOfReference(t *testing.T) { + var previous Binding + for _, version := range []string{"0.162.1", "0.163.0", "0.164.0"} { + binding, err := Select("codex-tui/"+version+" (Windows 10.0.26100; x86_64) WindowsTerminal", "same-account", "http") + if err != nil { + t.Fatal(err) + } + if binding.ClientVersion != version || binding.Version != LatestVersion() || !binding.Validated { + t.Fatalf("client and reference versions were conflated: %+v", binding) + } + transport, err := New(binding, Options{}) + if err != nil { + t.Fatal(err) + } + transport.CloseIdleConnections() + if previous.ClientVersion != "" && previous.Key() == binding.Key() { + t.Fatal("client versions shared a pool") + } + previous = binding + } +} diff --git a/backend/internal/pkg/codextransport/profile.go b/backend/internal/pkg/codextransport/profile.go index b6577116eea7..31e7d80a4ab2 100644 --- a/backend/internal/pkg/codextransport/profile.go +++ b/backend/internal/pkg/codextransport/profile.go @@ -80,6 +80,7 @@ type Binding struct { RequestZstd bool `json:"-"` ProfileID string `json:"profile_id"` Version string `json:"version"` + ClientVersion string `json:"client_version"` SourceCommit string `json:"source_commit"` System string `json:"system"` Arch string `json:"arch"` @@ -96,9 +97,8 @@ func Select(ua, scope, plane string) (Binding, error) { if len(m) == 0 { return Binding{}, &Error{"User-Agent has no supported fixed device profile"} } - if !SupportsVersion(m[1]) { - return Binding{}, &Error{fmt.Sprintf("version %s has no verified profile", m[1])} - } + // The archive version describes the sampled transport, not an admission + // list for client releases. Identity follows the selected client version. system := map[string]string{"Mac OS": "Darwin", "Windows": "Windows", "Ubuntu": "Linux"}[m[2]] arch := m[3] matched := false @@ -121,12 +121,12 @@ func Select(ua, scope, plane string) (Binding, error) { if plane != "ws" { plane = "http" } - return Binding{ProfileID: "codex-rust-" + verified.Version + "-r1", Version: verified.Version, SourceCommit: verified.SourceCommit, + return Binding{ProfileID: "codex-rust-" + verified.Version + "-r1", Version: verified.Version, ClientVersion: m[1], SourceCommit: verified.SourceCommit, System: system, Arch: arch, Strict: true, Validated: true, Scope: scope, Plane: plane, ReferenceConfiguration: "official binary with isolated custom CA"}, nil } func (b Binding) Key() string { - sum := sha256.Sum256([]byte(b.ProfileID + "\x00" + b.System + "\x00" + b.Arch + "\x00" + b.Scope + "\x00" + b.Plane)) + sum := sha256.Sum256([]byte(b.ProfileID + "\x00" + b.ClientVersion + "\x00" + b.System + "\x00" + b.Arch + "\x00" + b.Scope + "\x00" + b.Plane)) return fmt.Sprintf("%x", sum[:16]) } diff --git a/backend/internal/pkg/codextransport/transport_test.go b/backend/internal/pkg/codextransport/transport_test.go index 0c71332a1119..ba3b0fec965a 100644 --- a/backend/internal/pkg/codextransport/transport_test.go +++ b/backend/internal/pkg/codextransport/transport_test.go @@ -83,8 +83,8 @@ func TestStrictCodexTLSValidationAndHTTP2Streaming(t *testing.T) { } func TestStrictCodexUnsupportedAndCanceledRequests(t *testing.T) { - if _, err := Select("codex-tui/999.0.0 (Windows 10.0.26100; x86_64) WindowsTerminal", "test", "http"); err == nil { - t.Fatal("accepted unverified version") + if _, err := Select("codex-tui/0.163.0 (Windows 10.0.26100; arm64) WindowsTerminal", "test", "http"); err == nil { + t.Fatal("accepted unsupported device architecture") } if _, err := New(Binding{Version: LatestVersion()}, Options{}); err == nil { t.Fatal("accepted unverified binding") diff --git a/backend/internal/service/codex_version_follow_test.go b/backend/internal/service/codex_version_follow_test.go new file mode 100644 index 000000000000..70c408c4929f --- /dev/null +++ b/backend/internal/service/codex_version_follow_test.go @@ -0,0 +1,61 @@ +package service + +import ( + "context" + "net/http" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/pkg/codextransport" + "github.com/stretchr/testify/require" +) + +func TestCodexVersionFollowsStableWithoutProfileGate(t *testing.T) { + fingerprintPolicyTestState(t) + for _, test := range []struct{ name, manual, synced, syncEnabled, version, source string }{ + {"synced", "", "0.163.0", "true", "0.163.0", "synced"}, + {"future release", "", "0.164.0", "true", "0.164.0", "synced"}, + {"manual", "0.165.0", "0.163.0", "true", "0.165.0", "manual"}, + {"sync off retains discovery", "", "0.163.0", "false", "0.163.0", "synced"}, + {"default", "", "", "true", codexCLIVersion, "builtin"}, + {"minimum", "0.100.0", "", "false", codexCLIVersion, "builtin"}, + } { + t.Run(test.name, func(t *testing.T) { + svc := NewSettingService(&codexFingerprintSettingsRepo{values: map[string]string{ + SettingKeyOpenAICodexClientVersion: test.manual, SettingKeyOpenAICodexClientVersionSynced: test.synced, + SettingKeyOpenAICodexVersionAutoSyncEnabled: test.syncEnabled, + }}, nil) + view, err := svc.GetCodexFingerprintSettings(context.Background()) + require.NoError(t, err) + require.Equal(t, test.version, view.EffectiveVersion) + require.Equal(t, test.source, view.VersionSource) + require.False(t, view.VersionPendingValidation) + require.Equal(t, codextransport.LatestVersion(), view.VerifiedVersion) + publishCodexFingerprintSettings(view) + identity := resolveCodexOutboundIdentity("") + require.Equal(t, test.version, identity.version) + require.Contains(t, identity.userAgent, "/"+test.version+" ") + for _, plane := range []string{"http", "ws"} { + binding, err := codextransport.Select(identity.userAgent, "test", plane) + require.NoError(t, err) + require.Equal(t, test.version, binding.ClientVersion) + } + }) + } +} + +func TestCodexVersionSavePublishesNewIdentityWithoutChangingActiveAttempt(t *testing.T) { + fingerprintPolicyTestState(t) + repo := &codexFingerprintSettingsRepo{values: map[string]string{SettingKeyOpenAICodexClientVersionSynced: "0.163.0"}} + svc := NewSettingService(repo, nil) + require.NoError(t, svc.LoadCodexFingerprintSettings(context.Background())) + frozen := currentCodexFingerprintPolicy() + view, err := svc.UpdateCodexFingerprintSettings(context.Background(), CodexFingerprintSettings{Enabled: true, ClientVersion: "0.164.0", VersionAutoSyncEnabled: true}) + require.NoError(t, err) + require.Equal(t, "0.164.0", view.EffectiveVersion) + require.Equal(t, "0.163.0", resolveCodexOutboundIdentity("", frozen).version) + require.Equal(t, "0.164.0", resolveCodexOutboundIdentity("").version) + require.NotEqual(t, frozen.revision, currentCodexFingerprintPolicy().revision) + headers := http.Header{} + ApplyCodexCanonicalAuthIdentity(headers) + require.Contains(t, headers.Get("User-Agent"), "/0.164.0 ") +} diff --git a/backend/internal/service/openai_codex_version_sync_service_test.go b/backend/internal/service/openai_codex_version_sync_service_test.go index 444607a032c5..7d40593e53b5 100644 --- a/backend/internal/service/openai_codex_version_sync_service_test.go +++ b/backend/internal/service/openai_codex_version_sync_service_test.go @@ -303,9 +303,9 @@ func TestGetOpenAICodexClientVersionPriority(t *testing.T) { want string }{ {name: "面板覆写优先", override: "0.150.0", synced: "0.146.0", want: "0.150.0"}, - {name: "未验证同步值保持待支持", synced: "0.146.0", want: codexCLIVersion}, + {name: "同步稳定版直接生效", synced: "0.146.0", want: "0.146.0"}, {name: "两者皆空时用内置常量", want: codexCLIVersion}, - {name: "非法覆写且同步未验证时用已验证版本", override: "latest", synced: "0.146.0", want: codexCLIVersion}, + {name: "非法覆写使用同步版本", override: "latest", synced: "0.146.0", want: "0.146.0"}, {name: "非法同步值回退内置常量", synced: "not-a-version", want: codexCLIVersion}, } @@ -334,7 +334,7 @@ func TestGetOpenAICodexCanonicalUserAgentBuildsFromVersion(t *testing.T) { }}, nil) require.Equal(t, - "codex-tui/"+codexCLIVersion+codexCLIUserAgentSuffix, + "codex-tui/0.200.1"+codexCLIUserAgentSuffix, svc.GetOpenAICodexCanonicalUserAgent(context.Background()), ) } @@ -351,7 +351,7 @@ func TestGetOpenAICodexCanonicalUserAgentRebuildsPanelUAVersion(t *testing.T) { }}, nil) require.Equal(t, - "codex_cli_rs/"+codexCLIVersion+" (Ubuntu 22.4.0; x86_64) xterm-256color", + "codex_cli_rs/0.200.1 (Ubuntu 22.4.0; x86_64) xterm-256color", svc.GetOpenAICodexCanonicalUserAgent(context.Background()), ) }) @@ -363,7 +363,7 @@ func TestGetOpenAICodexCanonicalUserAgentRebuildsPanelUAVersion(t *testing.T) { }}, nil) require.Equal(t, - "codex_cli_rs/"+codexCLIVersion+" (Mac OS X 15.1.0; arm64) iTerm.app", + "codex_cli_rs/0.200.1 (Mac OS X 15.1.0; arm64) iTerm.app", svc.GetOpenAICodexCanonicalUserAgent(context.Background()), ) }) @@ -375,7 +375,7 @@ func TestGetOpenAICodexCanonicalUserAgentRebuildsPanelUAVersion(t *testing.T) { }}, nil) require.Equal(t, - "codex-tui/"+codexCLIVersion+" (Ubuntu 22.4.0; x86_64) WindowsTerminal (codex-tui; "+codexCLIVersion+")", + "codex-tui/0.200.1 (Ubuntu 22.4.0; x86_64) WindowsTerminal (codex-tui; 0.200.1)", svc.GetOpenAICodexCanonicalUserAgent(context.Background()), ) }) @@ -489,8 +489,8 @@ func TestGetOpenAICodexCanonicalUserAgentOutboundIdentity(t *testing.T) { name, manual, synced, want string }{ {"manual", "0.154.0", "0.155.1", "0.154.0"}, - {"synced", "", "0.155.1", codexCLIVersion}, - {"invalid_manual", "latest", "0.155.1", codexCLIVersion}, + {"synced", "", "0.155.1", "0.155.1"}, + {"invalid_manual", "latest", "0.155.1", "0.155.1"}, {"default", "", "", codexCLIVersion}, {"invalid_versions", "latest", "invalid", codexCLIVersion}, } { diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index 33564f7391fe..32d4b451772c 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -62,7 +62,7 @@ const ( // 陈旧版本会被优先丢弃(HTTP 200 + 流内 server_is_overloaded);非官方客户端配不出 // 官方身份时整体回退到本常量,因此它必须跟随官方 CLI 的当前发布版本, // 落后多个版本会让这些请求稳定落在被优先丢弃的一侧。 - codexCLIVersion = "0.162.1" + codexCLIVersion = "0.163.0" // Codex 限额快照仅用于后台展示/诊断,不需要每个成功请求都立即落库。 openAICodexSnapshotPersistMinInterval = 30 * time.Second // 配额自动暂停时,超过该时长仍未刷新的 used% 快照视为陈旧,不再据此暂停账号。 diff --git a/backend/internal/service/setting_codex_fingerprint.go b/backend/internal/service/setting_codex_fingerprint.go index 730d8a1c732c..ca3dd288ea32 100644 --- a/backend/internal/service/setting_codex_fingerprint.go +++ b/backend/internal/service/setting_codex_fingerprint.go @@ -35,6 +35,7 @@ type CodexFingerprintSettingsView struct { VersionPendingValidation bool `json:"version_pending_validation"` TransportPolicy string `json:"transport_policy"` EffectiveVersion string `json:"effective_version"` + VersionSource string `json:"version_source"` EffectiveUserAgent string `json:"effective_user_agent"` } @@ -180,23 +181,27 @@ func (s *SettingService) codexFingerprintSettingsFromValues(values map[string]st config.VersionAutoSyncEnabled = raw == "true" } synced := NormalizeCodexClientVersion(values[SettingKeyOpenAICodexClientVersionSynced]) - version := NormalizeCodexClientVersion(config.ClientVersion) - if version == "" { - version = synced - } - if config.ClientVersion == "" && !codextransport.SupportsVersion(version) { - version = codextransport.LatestVersion() - } - if version == "" || CompareVersions(version, codexUpstreamMinVersion) < 0 { - version = codextransport.LatestVersion() - } + version, source := resolveCodexClientVersion(config.ClientVersion, synced) ua := buildCodexCLIUserAgent(version) if _, paired, ok := openai.PairCodexClientIdentity(config.UserAgent); ok { if rebuilt := openai.SetCodexUserAgentVersion(paired, version); rebuilt != "" { ua = rebuilt } } - return CodexFingerprintSettingsView{CodexFingerprintSettings: config, SyncedVersion: synced, EffectiveVersion: version, EffectiveUserAgent: ua, VerifiedVersion: codextransport.LatestVersion(), VersionPendingValidation: synced != "" && CompareVersions(synced, version) > 0 && !codextransport.SupportsVersion(synced), TransportPolicy: "strict"}, nil + return CodexFingerprintSettingsView{CodexFingerprintSettings: config, SyncedVersion: synced, EffectiveVersion: version, VersionSource: source, EffectiveUserAgent: ua, VerifiedVersion: codextransport.LatestVersion(), VersionPendingValidation: false, TransportPolicy: "strict"}, nil +} + +// resolveCodexClientVersion is shared by the settings view and outbound resolver. +// Transport archive metadata never determines the selected client release. +func resolveCodexClientVersion(manual, synced string) (string, string) { + version, source := NormalizeCodexClientVersion(manual), "manual" + if version == "" { + version, source = NormalizeCodexClientVersion(synced), "synced" + } + if version == "" || CompareVersions(version, codexUpstreamMinVersion) < 0 { + return codexCLIVersion, "builtin" + } + return version, source } func publishCodexFingerprintSettings(view CodexFingerprintSettingsView) { @@ -227,8 +232,8 @@ func (s *SettingService) LoadCodexFingerprintSettings(ctx context.Context) error } func (s *SettingService) UpdateCodexFingerprintSettings(ctx context.Context, config CodexFingerprintSettings) (CodexFingerprintSettingsView, error) { - if config.ClientVersion != "" && !codextransport.SupportsVersion(config.ClientVersion) { - return CodexFingerprintSettingsView{}, fmt.Errorf("codex version %s has no verified transport profile", config.ClientVersion) + if config.ClientVersion != "" && NormalizeCodexClientVersion(config.ClientVersion) == "" { + return CodexFingerprintSettingsView{}, errors.New("client_version must be empty or a valid Codex version") } s.codexFingerprintMu.Lock() defer s.codexFingerprintMu.Unlock() diff --git a/backend/internal/service/setting_gateway_runtime.go b/backend/internal/service/setting_gateway_runtime.go index 50c6ed5c57c4..fe5904f28635 100644 --- a/backend/internal/service/setting_gateway_runtime.go +++ b/backend/internal/service/setting_gateway_runtime.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/Wei-Shaw/sub2api/internal/pkg/codextransport" "log/slog" "strconv" "strings" @@ -496,16 +495,7 @@ func (s *SettingService) GetOpenAICodexClientVersion(ctx context.Context) string }) return fallback, nil } - version := NormalizeCodexClientVersion(values[SettingKeyOpenAICodexClientVersion]) - if version == "" { - version = NormalizeCodexClientVersion(values[SettingKeyOpenAICodexClientVersionSynced]) - if !codextransport.SupportsVersion(version) { - version = codextransport.LatestVersion() - } - } - if version == "" { - version = fallback - } + version, _ := resolveCodexClientVersion(values[SettingKeyOpenAICodexClientVersion], values[SettingKeyOpenAICodexClientVersionSynced]) s.openAICodexVersionCache.Store(&cachedOpenAICodexClientVersion{ version: version, expiresAt: time.Now().Add(openAICodexClientVersionCacheTTL).UnixNano(), diff --git a/backend/internal/service/setting_update.go b/backend/internal/service/setting_update.go index 628f6b62af36..2c2204614514 100644 --- a/backend/internal/service/setting_update.go +++ b/backend/internal/service/setting_update.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/Wei-Shaw/sub2api/internal/pkg/codextransport" "log/slog" "math" "strconv" @@ -515,8 +514,8 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting updates[SettingKeyEnableClientDatelineNormalization] = strconv.FormatBool(settings.EnableClientDatelineNormalization) updates[SettingKeyAntigravityUserAgentVersion] = antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion) updates[SettingKeyOpenAICodexUserAgent] = strings.TrimSpace(settings.OpenAICodexUserAgent) - if settings.OpenAICodexClientVersion != "" && !codextransport.SupportsVersion(NormalizeCodexClientVersion(settings.OpenAICodexClientVersion)) { - return nil, fmt.Errorf("codex version has no verified transport profile") + if settings.OpenAICodexClientVersion != "" && NormalizeCodexClientVersion(settings.OpenAICodexClientVersion) == "" { + return nil, fmt.Errorf("client_version must be empty or a valid Codex version") } updates[SettingKeyOpenAICodexClientVersion] = NormalizeCodexClientVersion(settings.OpenAICodexClientVersion) updates[SettingKeyOpenAICodexVersionAutoSyncEnabled] = strconv.FormatBool(settings.OpenAICodexVersionAutoSyncEnabled) diff --git a/frontend/src/api/admin/codexFingerprint.ts b/frontend/src/api/admin/codexFingerprint.ts index d0cfb5716ea6..cc3d0fd10d90 100644 --- a/frontend/src/api/admin/codexFingerprint.ts +++ b/frontend/src/api/admin/codexFingerprint.ts @@ -15,6 +15,7 @@ export interface CodexFingerprintSettingsView extends CodexFingerprintSettings { transport_policy?: 'strict' synced_version: string effective_version: string + version_source?: 'manual' | 'synced' | 'builtin' effective_user_agent: string } @@ -31,7 +32,7 @@ export interface CodexFingerprintAccountView { fingerprint_mode_configured: string fingerprint_mode_effective: CodexFingerprintMode fingerprint_reason: string - transport_profile?: { profile_id: string; version: string; source_commit: string; system: string; arch: string; strict: boolean; validated: boolean; plane: string } + transport_profile?: { profile_id: string; version: string; client_version?: string; source_commit: string; system: string; arch: string; strict: boolean; validated: boolean; plane: string } transport_profile_error?: string } device_identity: { diff --git a/frontend/src/components/admin/CodexFingerprintIdentity.vue b/frontend/src/components/admin/CodexFingerprintIdentity.vue index e9705aa452eb..df1a8501642e 100644 --- a/frontend/src/components/admin/CodexFingerprintIdentity.vue +++ b/frontend/src/components/admin/CodexFingerprintIdentity.vue @@ -35,7 +35,8 @@
{{ view.identity.transport_profile_error }}
{{ error }}
+{{ error }}
-
- {{ text('每页 50 个账号;错误、限流和停用账号仍可明确选择。查看与选择不会调用模型。', '50 accounts per page. Error, limited and disabled accounts remain selectable. Browsing and selecting never calls a model.') }}
- -{{ t('admin.pelicanTests.noAccounts') }}
-{{ text('每个账号/模型组合各执行相同次数,须为正整数;当前最多', 'Each account/model combination runs this many times. Enter a positive integer, up to') }} {{ repeatMaximum }} {{ text('次,整批最多 5000 条测试。', 'times, with at most 5000 tests per task.') }}
-{{ bulkMessage }}
-{{ options[id].capability_reason }}
-{{ error }}
+ {{ rows.length }} {{ text('个账号/模型组合', 'account/model combinations') }} × {{ repeatCount }} {{ text('次', 'runs') }} = {{ totalTests }} {{ text('条测试', 'tests') }}
-{{ t('admin.pelicanTests.budgetBoundary') }}
-{{ text('共享 10 个模型请求执行位,同账号不同模型可并行,同模型重复依次执行,仍受账号自身并发额度限制。关闭此页面后任务继续;停止需要明确点击任务的停止按钮。', 'Ten shared model request slots. Different models on one account can run in parallel; repeats of the same model run in order. Account concurrency limits still apply. Tasks continue after this page closes; use the task Stop button to cancel.') }}
+ +{{ text('至少选择一个检测项目。', 'Select at least one test.') }}
+{{ text('次数须为正整数,当前每个模型最多', 'Enter a positive integer, up to') }} {{ repeatMaximum }} {{ text('次;整批最多 5000 条测试。', 'runs per model; at most 5000 tests per task.') }}
+{{ t('admin.pelicanTests.budgetBoundary') }}
+{{ bulkMessage }}
+{{ text('从账号列表开始', 'Start with an account') }}
{{ text('选择账号后,在这里配置模型;同一账号可以添加多个模型。', 'Choose accounts to configure their models here. Each account can use multiple models.') }}
{{ options[entry.id].capability_reason }}
+{{ text('正在读取可用模型…', 'Loading available models…') }}
+{{ issue(row) }}
{{ t('admin.pelicanTests.mappedModel') }}: {{ modelOption(row)?.upstream_model }}
+{{ PELICAN_PROMPT }}
{{ text('共享 10 个模型请求执行位,同账号不同模型可并行,同模型重复依次执行,并遵守账号自身并发额度。视觉审核使用同一账号和模型;不支持图片时显示未审核。关闭页面后任务继续。', 'Ten shared request slots; different models on one account can run in parallel, while repeats run in order. Account limits apply. Review uses the same account and model; models without image input remain unreviewed. Tasks continue after this page closes.') }}
{{ PELICAN_PROMPT }}