mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:48:43 +08:00
feat(keys): expand Grok client samples and tune free soft-gate default
Ship Use Key templates that match Grok Build / Codex best practice: env vars + multi-model config.toml with api_backend=responses, env_key over hardcoded secrets, and clearer shell/path guidance for Claude/Codex/OpenCode. Also set free_quota_token_limit default to 500k (24h soft-gate), clean up personal-dev-only comments, and keep billing test fixtures aligned.
This commit is contained in:
@@ -124,7 +124,7 @@ type Group struct {
|
||||
// VideoModelPrices 可选按模型族×分辨率覆盖视频每秒单价 (USD/s)。
|
||||
VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"`
|
||||
// Codex alpha/search 网页搜索单次价格(USD/次);null 表示使用默认价 0.01
|
||||
WebSearchPricePerCall *float64 `json:"web_search_price_per_call"`
|
||||
WebSearchPricePerCall *float64 `json:"web_search_price_per_call"`
|
||||
SearchPricePer1k *float64 `json:"search_price_per_1k"`
|
||||
AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min"`
|
||||
AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars"`
|
||||
|
||||
@@ -282,7 +282,7 @@ func anthropicThresholdCandidates(account *Account) []*accountSchedulingThreshol
|
||||
// kiro_sched_* / antigravity_sched_* extras are still written purely as
|
||||
// observability snapshots.
|
||||
|
||||
// grokThresholdCandidates mirrors personal-dev: only header-projected
|
||||
// grokThresholdCandidates uses only header-projected
|
||||
// grok_sched_utilization / grok_sched_reset_at (rolling quota window, reset
|
||||
// capped at ~25h when written). Official billing 7d/30d windows are not used
|
||||
// for auto-pause here.
|
||||
|
||||
@@ -310,7 +310,7 @@ func TestEvaluateAccountSchedulingThreshold_GrokUsesConfiguredThresholds(t *test
|
||||
|
||||
func TestEvaluateAccountSchedulingThreshold_GrokUsesOnlyHeaderQuotaWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
// personal-dev: billing seven_day/thirty_day must not drive pause; only grok_sched_*.
|
||||
// Billing seven_day/thirty_day must not drive pause; only grok_sched_* may.
|
||||
now := time.Date(2026, 6, 3, 12, 0, 0, 0, time.UTC)
|
||||
weeklyEnd := now.Add(3 * time.Hour)
|
||||
weeklyPct := 99.0
|
||||
@@ -327,7 +327,7 @@ func TestEvaluateAccountSchedulingThreshold_GrokUsesOnlyHeaderQuotaWindow(t *tes
|
||||
},
|
||||
}
|
||||
decision := EvaluateAccountSchedulingThreshold(account, map[string]int{PlatformGrok: 90}, now)
|
||||
require.False(t, decision.ShouldPause, "high billing % alone must not pause under personal-dev windows")
|
||||
require.False(t, decision.ShouldPause, "high billing % alone must not pause under scheduling windows")
|
||||
|
||||
account.Extra["grok_sched_utilization"] = 95.0
|
||||
decision = EvaluateAccountSchedulingThreshold(account, map[string]int{PlatformGrok: 90}, now)
|
||||
|
||||
@@ -199,21 +199,21 @@ type UsageInfo struct {
|
||||
AntigravityQuota map[string]*AntigravityModelQuota `json:"antigravity_quota,omitempty"`
|
||||
|
||||
// Grok / xAI 被动额度快照
|
||||
GrokRequestQuota *xai.QuotaWindow `json:"grok_request_quota,omitempty"`
|
||||
GrokTokenQuota *xai.QuotaWindow `json:"grok_token_quota,omitempty"`
|
||||
GrokRetryAfterSeconds *int `json:"grok_retry_after_seconds,omitempty"`
|
||||
GrokEntitlementStatus string `json:"grok_entitlement_status,omitempty"`
|
||||
GrokQuotaSnapshotState string `json:"grok_quota_snapshot_state,omitempty"`
|
||||
GrokLastQuotaProbeAt string `json:"grok_last_quota_probe_at,omitempty"`
|
||||
GrokLastHeadersSeenAt string `json:"grok_last_headers_seen_at,omitempty"`
|
||||
GrokLastStatusCode int `json:"grok_last_status_code,omitempty"`
|
||||
GrokFreeTokenLimit int64 `json:"grok_free_token_limit,omitempty"`
|
||||
GrokLocalUsage *WindowStats `json:"grok_local_usage,omitempty"`
|
||||
GrokLocalUsage24h *WindowStats `json:"grok_local_usage_24h,omitempty"`
|
||||
GrokLocalUsage7d *WindowStats `json:"grok_local_usage_7d,omitempty"`
|
||||
GrokLocalUsageMonthly *WindowStats `json:"grok_local_usage_monthly,omitempty"`
|
||||
GrokRequestQuota *xai.QuotaWindow `json:"grok_request_quota,omitempty"`
|
||||
GrokTokenQuota *xai.QuotaWindow `json:"grok_token_quota,omitempty"`
|
||||
GrokRetryAfterSeconds *int `json:"grok_retry_after_seconds,omitempty"`
|
||||
GrokEntitlementStatus string `json:"grok_entitlement_status,omitempty"`
|
||||
GrokQuotaSnapshotState string `json:"grok_quota_snapshot_state,omitempty"`
|
||||
GrokLastQuotaProbeAt string `json:"grok_last_quota_probe_at,omitempty"`
|
||||
GrokLastHeadersSeenAt string `json:"grok_last_headers_seen_at,omitempty"`
|
||||
GrokLastStatusCode int `json:"grok_last_status_code,omitempty"`
|
||||
GrokFreeTokenLimit int64 `json:"grok_free_token_limit,omitempty"`
|
||||
GrokLocalUsage *WindowStats `json:"grok_local_usage,omitempty"`
|
||||
GrokLocalUsage24h *WindowStats `json:"grok_local_usage_24h,omitempty"`
|
||||
GrokLocalUsage7d *WindowStats `json:"grok_local_usage_7d,omitempty"`
|
||||
GrokLocalUsageMonthly *WindowStats `json:"grok_local_usage_monthly,omitempty"`
|
||||
// ThirtyDay is the official monthly billing window (used/monthlyLimit %).
|
||||
ThirtyDay *UsageProgress `json:"thirty_day,omitempty"`
|
||||
ThirtyDay *UsageProgress `json:"thirty_day,omitempty"`
|
||||
GrokBilling *xai.BillingSummary `json:"grok_billing,omitempty"`
|
||||
|
||||
// Antigravity 账号级信息
|
||||
|
||||
@@ -56,25 +56,25 @@ type APIKeyAuthUserSnapshot struct {
|
||||
|
||||
// APIKeyAuthGroupSnapshot 分组快照
|
||||
type APIKeyAuthGroupSnapshot struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
IsExclusive bool `json:"is_exclusive"`
|
||||
Status string `json:"status"`
|
||||
SubscriptionType string `json:"subscription_type"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
DailyLimitUSD *float64 `json:"daily_limit_usd,omitempty"`
|
||||
WeeklyLimitUSD *float64 `json:"weekly_limit_usd,omitempty"`
|
||||
MonthlyLimitUSD *float64 `json:"monthly_limit_usd,omitempty"`
|
||||
AllowImageGeneration bool `json:"allow_image_generation"`
|
||||
AllowBatchImageGeneration bool `json:"allow_batch_image_generation"`
|
||||
ImageRateIndependent bool `json:"image_rate_independent"`
|
||||
ImageRateMultiplier float64 `json:"image_rate_multiplier"`
|
||||
ImagePrice1K *float64 `json:"image_price_1k,omitempty"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k,omitempty"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k,omitempty"`
|
||||
VideoRateIndependent bool `json:"video_rate_independent"`
|
||||
VideoRateMultiplier float64 `json:"video_rate_multiplier"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
IsExclusive bool `json:"is_exclusive"`
|
||||
Status string `json:"status"`
|
||||
SubscriptionType string `json:"subscription_type"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
DailyLimitUSD *float64 `json:"daily_limit_usd,omitempty"`
|
||||
WeeklyLimitUSD *float64 `json:"weekly_limit_usd,omitempty"`
|
||||
MonthlyLimitUSD *float64 `json:"monthly_limit_usd,omitempty"`
|
||||
AllowImageGeneration bool `json:"allow_image_generation"`
|
||||
AllowBatchImageGeneration bool `json:"allow_batch_image_generation"`
|
||||
ImageRateIndependent bool `json:"image_rate_independent"`
|
||||
ImageRateMultiplier float64 `json:"image_rate_multiplier"`
|
||||
ImagePrice1K *float64 `json:"image_price_1k,omitempty"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k,omitempty"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k,omitempty"`
|
||||
VideoRateIndependent bool `json:"video_rate_independent"`
|
||||
VideoRateMultiplier float64 `json:"video_rate_multiplier"`
|
||||
VideoPrice480P *float64 `json:"video_price_480p,omitempty"`
|
||||
VideoPrice720P *float64 `json:"video_price_720p,omitempty"`
|
||||
VideoPrice1080P *float64 `json:"video_price_1080p,omitempty"`
|
||||
@@ -84,9 +84,9 @@ type APIKeyAuthGroupSnapshot struct {
|
||||
AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min,omitempty"`
|
||||
AudioTTSPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars,omitempty"`
|
||||
AudioSTTPricePerHour *float64 `json:"audio_stt_price_per_hour,omitempty"`
|
||||
ClaudeCodeOnly bool `json:"claude_code_only"`
|
||||
FallbackGroupID *int64 `json:"fallback_group_id,omitempty"`
|
||||
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request,omitempty"`
|
||||
ClaudeCodeOnly bool `json:"claude_code_only"`
|
||||
FallbackGroupID *int64 `json:"fallback_group_id,omitempty"`
|
||||
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request,omitempty"`
|
||||
|
||||
// Model routing is used by gateway account selection, so it must be part of auth cache snapshot.
|
||||
// Only anthropic groups use these fields; others may leave them empty.
|
||||
|
||||
@@ -1527,7 +1527,6 @@ func (s *BillingService) CalculateAudioCost(mode string, durationOrUnits float64
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// CalculateImageCost 计算图片生成费用
|
||||
// model: 请求的模型名称(用于获取 LiteLLM 默认价格)
|
||||
// imageSize: 图片尺寸 "1K", "2K", "4K"
|
||||
|
||||
@@ -158,8 +158,6 @@ func (s *stickyGatewayCacheHotpathStub) ReleaseGrokVideoBilled(_ context.Context
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (s *modelsListAccountRepoStub) ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]Account, error) {
|
||||
s.listByGroupCalls.Add(1)
|
||||
if s.err != nil {
|
||||
|
||||
@@ -291,7 +291,6 @@ func (m *mockGatewayCacheForPlatform) ReleaseGrokVideoBilled(_ context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
type mockGroupRepoForGateway struct {
|
||||
groups map[int64]*Group
|
||||
getByIDCalls int
|
||||
|
||||
@@ -319,7 +319,6 @@ func (m *mockGatewayCacheForGemini) ReleaseGrokVideoBilled(_ context.Context, _
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// TestGeminiMessagesCompatService_SelectAccountForModelWithExclusions_GeminiPlatform 测试 Gemini 单平台选择
|
||||
func TestGeminiMessagesCompatService_SelectAccountForModelWithExclusions_GeminiPlatform(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
// - free_quota_stats_cache_seconds (int, default 5) — bound hot-path aggregate query frequency
|
||||
//
|
||||
// Soft-gate applies only to *explicit* free OAuth (subscription_tier/plan_type ==
|
||||
// "free"), matching personal-dev. Media/cache free detection uses isKnownGrokFreeAccount.
|
||||
// "free"). Media/cache free detection uses isKnownGrokFreeAccount instead.
|
||||
// Admin paths (QueryQuota / import probe) never call this filter.
|
||||
|
||||
const (
|
||||
@@ -86,7 +86,7 @@ func calculateGrokFreeQuotaSoftGateTokens(limit int64, percent int) int64 {
|
||||
}
|
||||
|
||||
// isExplicitGrokFreeOAuthAccount decides whether the free soft-gate applies.
|
||||
// personal-dev contract: only OAuth accounts with credentials/extra
|
||||
// Contract: only OAuth accounts with credentials/extra
|
||||
// subscription_tier or plan_type exactly "free" (case-insensitive). Inferred
|
||||
// free / basic / blank plan do not soft-gate.
|
||||
func isExplicitGrokFreeOAuthAccount(account *Account) bool {
|
||||
|
||||
@@ -151,7 +151,7 @@ func TestIsExplicitGrokFreeOAuthAccount_OnlyExactFree(t *testing.T) {
|
||||
require.False(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeAPIKey, Credentials: map[string]any{"subscription_tier": "free"}}))
|
||||
require.True(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"subscription_tier": "FREE"}}))
|
||||
require.True(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"plan_type": "free"}}))
|
||||
// basic / inferred free are NOT soft-gated (personal-dev contract).
|
||||
// basic / inferred free are NOT soft-gated (only an explicit "free" tier is).
|
||||
require.False(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"subscription_tier": "basic"}}))
|
||||
require.False(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth}))
|
||||
}
|
||||
|
||||
@@ -196,7 +196,6 @@ func (c *schedulerTestGatewayCache) ReleaseGrokVideoBilled(_ context.Context, _
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func newSchedulerTestOpenAIWSV2Config() *config.Config {
|
||||
cfg := &config.Config{}
|
||||
cfg.Gateway.OpenAIWS.Enabled = true
|
||||
|
||||
@@ -148,7 +148,6 @@ func (c *comboCacheAndStore) ReleaseGrokVideoBilled(_ context.Context, _ string)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (c *comboCacheAndStore) SetCyberSessionBlocked(ctx context.Context, key string, ttl time.Duration) error {
|
||||
return c.store.SetCyberSessionBlocked(ctx, key, ttl)
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ func applyGrokFreeToolCacheRoute(body, intentSourceBody []byte, account *Account
|
||||
return appendGrokFreeCacheNativeToolsWithPolicy(body, allowPureClientTools, allowFunctionSearch)
|
||||
}
|
||||
|
||||
// isKnownGrokFreeAccount matches personal-dev free-tier recognition used for
|
||||
// isKnownGrokFreeAccount recognizes free-tier Grok accounts, used for
|
||||
// Free cache routing / media free_tier blocks (broader than soft-gate).
|
||||
// Soft-gate uses isExplicitGrokFreeOAuthAccount (exact "free" only).
|
||||
func isKnownGrokFreeAccount(account *Account) bool {
|
||||
@@ -322,7 +322,7 @@ func isKnownGrokFreeAccount(account *Account) bool {
|
||||
paidSignal = true
|
||||
}
|
||||
}
|
||||
// personal-dev: usage % or monthly dollar cap is paid evidence.
|
||||
// Usage % or a monthly dollar cap is evidence of a paid plan.
|
||||
if billing.UsagePercent != nil || billing.UsedPercent != nil ||
|
||||
(billing.MonthlyLimitCents != nil && *billing.MonthlyLimitCents > 0) {
|
||||
paidSignal = true
|
||||
@@ -347,7 +347,7 @@ func isKnownGrokFreeAccount(account *Account) bool {
|
||||
inferredFreeSignal = true
|
||||
}
|
||||
}
|
||||
// personal-dev: only credentials subscription_tier (not plan_type / extra keys).
|
||||
// Only credentials subscription_tier is authoritative here (not plan_type / extra keys).
|
||||
if tier := strings.TrimSpace(account.GetCredential("subscription_tier")); tier != "" {
|
||||
if isGrokFreeSubscriptionTier(tier) {
|
||||
freeSignal = true
|
||||
|
||||
@@ -2041,10 +2041,10 @@ func TestGrokVideoBillingUsesSeparateVideoRateMultiplier(t *testing.T) {
|
||||
|
||||
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
|
||||
Result: &OpenAIForwardResult{
|
||||
RequestID: "video-request-123",
|
||||
ResponseID: "video-request-123",
|
||||
Model: "grok-imagine-video-1.5",
|
||||
BillingModel: "grok-imagine-video-1.5",
|
||||
RequestID: "video-request-123",
|
||||
ResponseID: "video-request-123",
|
||||
Model: "grok-imagine-video-1.5",
|
||||
BillingModel: "grok-imagine-video-1.5",
|
||||
// Pure video completion clears ImageCount (handler contract).
|
||||
ImageCount: 0,
|
||||
VideoCount: 1,
|
||||
@@ -2166,7 +2166,7 @@ func TestOpenAIGatewayServiceRecordUsage_GroupImagePriceOverridesChannelImagePri
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, usageRepo.lastLog)
|
||||
require.Equal(t, 0, usageRepo.lastLog.ImageCount)
|
||||
require.Equal(t, 1, usageRepo.lastLog.ImageCount)
|
||||
require.Equal(t, ImageBillingSize2K, *usageRepo.lastLog.ImageSize)
|
||||
require.InDelta(t, 0.021, usageRepo.lastLog.TotalCost, 1e-12)
|
||||
require.InDelta(t, 0.021, usageRepo.lastLog.ActualCost, 1e-12)
|
||||
|
||||
@@ -678,7 +678,6 @@ func (c *stubGatewayCache) ReleaseGrokVideoBilled(_ context.Context, _ string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func TestOpenAISelectAccountWithLoadAwareness_FiltersUnschedulable(t *testing.T) {
|
||||
now := time.Now()
|
||||
resetAt := now.Add(10 * time.Minute)
|
||||
|
||||
@@ -207,7 +207,6 @@ func (c *openAIWSStateStoreTimeoutProbeCache) ReleaseGrokVideoBilled(_ context.C
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func TestOpenAIWSStateStore_RedisOpsUseShortTimeout(t *testing.T) {
|
||||
probe := &openAIWSStateStoreTimeoutProbeCache{}
|
||||
store := NewOpenAIWSStateStore(probe)
|
||||
|
||||
@@ -118,8 +118,6 @@ func (c StubGatewayCache) ReleaseGrokVideoBilled(_ context.Context, _ string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================================================
|
||||
// StubSessionLimitCache — service.SessionLimitCache 的空实现
|
||||
// ============================================================
|
||||
|
||||
@@ -449,7 +449,7 @@ gateway:
|
||||
# config compatibility only and is ignored by the server.
|
||||
password_auth_enabled: false
|
||||
free_quota_soft_gate_enabled: true
|
||||
free_quota_token_limit: 1000000
|
||||
free_quota_token_limit: 500000
|
||||
free_quota_soft_gate_percent: 95
|
||||
free_quota_window_hours: 24
|
||||
free_quota_stats_cache_seconds: 5
|
||||
|
||||
@@ -203,7 +203,7 @@ const planBadgeClass = computed(() => {
|
||||
if (normalizedPlanType.value === 'abnormal') {
|
||||
return 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400'
|
||||
}
|
||||
// Free stays muted gray; paid Grok tiers get distinct colors (aligned with personal-dev).
|
||||
// Free stays muted gray; paid Grok tiers get distinct colors.
|
||||
if (normalizedPlanType.value === 'free' || normalizedPlanType.value === 'basic') {
|
||||
return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('PlatformTypeBadge Grok plans', () => {
|
||||
expect(wrapper.text()).toContain('SuperGrok Heavy')
|
||||
expect(wrapper.find('[data-testid="grok-plan-icon"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="grok-free-plan-icon"]').exists()).toBe(false)
|
||||
// Heavy uses purple plan chip (personal-dev parity)
|
||||
// Heavy uses purple plan chip
|
||||
expect(wrapper.html()).toContain('bg-purple-100')
|
||||
|
||||
await wrapper.setProps({ platform: 'openai', planType: 'free' })
|
||||
|
||||
@@ -452,9 +452,14 @@ const platformNote = computed(() => {
|
||||
? t('keys.useKeyModal.grok.codexNoteWindows')
|
||||
: t('keys.useKeyModal.grok.codexNote')
|
||||
}
|
||||
return activeTab.value === 'windows'
|
||||
? t('keys.useKeyModal.grok.noteWindows')
|
||||
: t('keys.useKeyModal.grok.note')
|
||||
// Grok CLI: shell-specific path guidance (env + ~/.grok/config.toml).
|
||||
if (activeClientTab.value === 'grok' && (activeTab.value === 'cmd' || activeTab.value === 'powershell')) {
|
||||
return t('keys.useKeyModal.grok.noteWindows')
|
||||
}
|
||||
if (activeClientTab.value === 'grok' && activeTab.value === 'windows') {
|
||||
return t('keys.useKeyModal.grok.noteWindows')
|
||||
}
|
||||
return t('keys.useKeyModal.grok.note')
|
||||
default:
|
||||
return t('keys.useKeyModal.note')
|
||||
}
|
||||
@@ -754,62 +759,193 @@ http_headers = { "x-openai-actor-authorization" = "local-image-extension" }`
|
||||
return 'requires_openai_auth = true'
|
||||
}
|
||||
|
||||
function generateGrokFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
||||
const isWindows = activeTab.value === 'windows'
|
||||
const configDir = isWindows ? '%userprofile%\\.grok' : '~/.grok'
|
||||
const configContent = `[models]
|
||||
default = "grok"
|
||||
web_search = "grok"
|
||||
function joinConfigPath(dir: string, file: string, windows: boolean): string {
|
||||
if (!windows) return `${dir}/${file}`
|
||||
return `${dir}\\${file}`
|
||||
}
|
||||
|
||||
[model."grok"]
|
||||
model = "grok-4.5"
|
||||
base_url = "${baseUrl}"
|
||||
name = "Grok 4.5"
|
||||
api_key = "${apiKey}"
|
||||
function generateGrokFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
||||
// Prefer unix/cmd/powershell when shell tabs are shown; fall back to windows tab.
|
||||
const shell = activeTab.value
|
||||
const isWindowsPath = shell === 'windows' || shell === 'cmd' || shell === 'powershell'
|
||||
const configDir = isWindowsPath ? '%userprofile%\\.grok' : '~/.grok'
|
||||
|
||||
let envPath: string
|
||||
let envContent: string
|
||||
switch (shell) {
|
||||
case 'cmd':
|
||||
envPath = 'Command Prompt'
|
||||
envContent = `set GROK_MODELS_BASE_URL=${baseUrl}
|
||||
set XAI_API_KEY=${apiKey}`
|
||||
break
|
||||
case 'powershell':
|
||||
case 'windows':
|
||||
envPath = 'PowerShell'
|
||||
envContent = `$env:GROK_MODELS_BASE_URL="${baseUrl}"
|
||||
$env:XAI_API_KEY="${apiKey}"`
|
||||
break
|
||||
default:
|
||||
envPath = 'Terminal'
|
||||
envContent = `export GROK_MODELS_BASE_URL="${baseUrl}"
|
||||
export XAI_API_KEY="${apiKey}"`
|
||||
}
|
||||
|
||||
// Shape follows official Grok Build docs (docs.x.ai/build/settings + custom-models guide).
|
||||
// Text models only (Responses). Image/video: grok-imagine-image / grok-imagine-video on media endpoints.
|
||||
// Credential order: api_key field → env_key → signed-in session → XAI_API_KEY global fallback.
|
||||
const configContent = `# Grok Build CLI → Sub2API Grok group (API key auth).
|
||||
# Official docs: https://docs.x.ai/build/settings
|
||||
# Verify after save: grok inspect
|
||||
#
|
||||
# Text / agent models only. Image & video use separate Imagine model IDs on media endpoints
|
||||
# (e.g. grok-imagine-image, grok-imagine-video) — not the catalog below.
|
||||
#
|
||||
# IMPORTANT: api_backend must be "responses" for Sub2API Grok (POST /v1/responses).
|
||||
# If omitted, Grok Build defaults to chat_completions (/v1/chat/completions).
|
||||
|
||||
# Global inference base (same role as env GROK_MODELS_BASE_URL).
|
||||
# Grok also lists models from {models_base_url}/models when this is set.
|
||||
[endpoints]
|
||||
models_base_url = "${baseUrl}"
|
||||
|
||||
# Prefer env_key over hardcoding api_key (never commit secrets).
|
||||
# Also export GROK_MODELS_BASE_URL + XAI_API_KEY in the shell block above.
|
||||
|
||||
[model."grok-4.5"]
|
||||
model = "grok-4.5" # id sent to the API
|
||||
name = "Grok 4.5" # shown in /model picker
|
||||
description = "Grok 4.5 via Sub2API (Responses)"
|
||||
# base_url inherits from [endpoints].models_base_url; override only if needed:
|
||||
# base_url = "${baseUrl}"
|
||||
env_key = "XAI_API_KEY" # or: api_key = "${apiKey}" (not recommended)
|
||||
api_backend = "responses" # chat_completions | responses | messages
|
||||
context_window = 1000000 # drives auto-compaction timing
|
||||
# Optional sampling (global defaults can live under [models] instead):
|
||||
# temperature = 0.7
|
||||
# top_p = 0.95
|
||||
# max_completion_tokens = 8192
|
||||
# Server-side (backend) web_search tools — only if your gateway exposes them:
|
||||
supports_backend_search = true
|
||||
|
||||
[model."grok-build-0.1"]
|
||||
model = "grok-build-0.1"
|
||||
name = "Grok Build"
|
||||
description = "Coding / agent sessions (xAI recommends grok-build* for coding)"
|
||||
env_key = "XAI_API_KEY"
|
||||
api_backend = "responses"
|
||||
context_window = 256000
|
||||
supports_backend_search = true
|
||||
|
||||
# Text multi-agent / client web_search sub-agent (NOT Imagine image/video).
|
||||
[model."grok-4.20-multi-agent-0309"]
|
||||
model = "grok-4.20-multi-agent-0309"
|
||||
name = "Grok 4.20 Multi Agent (text / web_search)"
|
||||
description = "Text multi-agent; use for web_search sub-agent, not image/video"
|
||||
env_key = "XAI_API_KEY"
|
||||
api_backend = "responses"
|
||||
context_window = 1000000
|
||||
supports_backend_search = true`
|
||||
supports_backend_search = true
|
||||
|
||||
return [{
|
||||
path: `${configDir}/config.toml`,
|
||||
content: configContent,
|
||||
hint: t('keys.useKeyModal.grok.configTomlHint')
|
||||
}]
|
||||
[model."grok-4.3"]
|
||||
model = "grok-4.3"
|
||||
name = "Grok 4.3"
|
||||
env_key = "XAI_API_KEY"
|
||||
api_backend = "responses"
|
||||
context_window = 1000000
|
||||
supports_backend_search = true
|
||||
|
||||
# Optional short alias for /model grok:
|
||||
# [model."grok"]
|
||||
# model = "grok-4.5"
|
||||
# name = "Grok"
|
||||
# env_key = "XAI_API_KEY"
|
||||
# api_backend = "responses"
|
||||
# context_window = 1000000
|
||||
# supports_backend_search = true
|
||||
|
||||
[models]
|
||||
# xAI recommends grok-build* for coding/agent sessions; use grok-4.5 for general chat.
|
||||
default = "grok-4.5"
|
||||
web_search = "grok-4.5" # client-side web_search tool model (must exist as [model.*])
|
||||
# Optional environment-wide sampling defaults (per-model values win):
|
||||
# temperature = 0.7
|
||||
# top_p = 0.95
|
||||
# max_completion_tokens = 8192
|
||||
# max_retries = 8`
|
||||
|
||||
return [
|
||||
{ path: envPath, content: envContent },
|
||||
{
|
||||
path: joinConfigPath(configDir, 'config.toml', isWindowsPath),
|
||||
content: configContent,
|
||||
hint: t('keys.useKeyModal.grok.configTomlHint')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function generateGrokCodexFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
||||
const isWindows = activeTab.value === 'windows'
|
||||
const configPath = isWindows
|
||||
? '%USERPROFILE%\\.codex\\config.toml'
|
||||
: '~/.codex/config.toml'
|
||||
const configContent = `model_provider = "sub2api_grok"
|
||||
model = "grok-4.5"
|
||||
review_model = "grok-4.5"
|
||||
model_reasoning_effort = "xhigh"
|
||||
model_context_window = 1000000
|
||||
// Codex config reference: wire_api = "responses" only; prefer env_key over experimental_bearer_token.
|
||||
// Non-OpenAI gateways should set supports_websockets = false (HTTP/SSE).
|
||||
const shell = activeTab.value
|
||||
const isWindowsPath = shell === 'windows' || shell === 'cmd' || shell === 'powershell'
|
||||
const configDir = isWindowsPath ? '%userprofile%\\.codex' : '~/.codex'
|
||||
|
||||
[model_providers.sub2api_grok]
|
||||
let envPath: string
|
||||
let envContent: string
|
||||
switch (shell) {
|
||||
case 'cmd':
|
||||
envPath = 'Command Prompt'
|
||||
envContent = `set SUB2API_API_KEY=${apiKey}`
|
||||
break
|
||||
case 'powershell':
|
||||
case 'windows':
|
||||
envPath = 'PowerShell'
|
||||
envContent = `$env:SUB2API_API_KEY="${apiKey}"`
|
||||
break
|
||||
default:
|
||||
envPath = 'Terminal'
|
||||
envContent = `export SUB2API_API_KEY="${apiKey}"`
|
||||
}
|
||||
|
||||
const configContent = `# Codex CLI → Sub2API Grok group
|
||||
# Docs: Codex config reference (model_providers.*, wire_api = "responses")
|
||||
#
|
||||
# Text models only. Image/video: grok-imagine-image / grok-imagine-video on media endpoints.
|
||||
# Switch model: grok-4.5 | grok-4.3 | grok-build-0.1 | grok-4.20-multi-agent-0309 (text / web_search)
|
||||
|
||||
model_provider = "sub2api"
|
||||
model = "grok-4.5"
|
||||
# Optional:
|
||||
# review_model = "grok-4.5"
|
||||
# model_reasoning_effort = "medium"
|
||||
# model_context_window = 1000000
|
||||
# disable_response_storage = true
|
||||
# network_access = "enabled"
|
||||
# windows_wsl_setup_acknowledged = true
|
||||
|
||||
[model_providers.sub2api]
|
||||
name = "Sub2API Grok"
|
||||
base_url = "${baseUrl}"
|
||||
# Prefer env_key (variable NAME). Do not combine with experimental_bearer_token.
|
||||
env_key = "SUB2API_API_KEY"
|
||||
# Fallback only if you cannot set env (discouraged — keeps secret on disk):
|
||||
# experimental_bearer_token = "${apiKey}"
|
||||
wire_api = "responses"
|
||||
supports_websockets = true
|
||||
# API-key providers: do not require ChatGPT OAuth login
|
||||
requires_openai_auth = false
|
||||
# Grok/Sub2API path is HTTP/SSE; disable WS (Codex may otherwise try WebSocket first)
|
||||
supports_websockets = false
|
||||
|
||||
[features]
|
||||
responses_websockets_v2 = true`
|
||||
const environmentContent = isWindows
|
||||
? `$env:SUB2API_API_KEY="${apiKey}"`
|
||||
: `export SUB2API_API_KEY="${apiKey}"`
|
||||
# Optional:
|
||||
# [features]
|
||||
# goals = true`
|
||||
|
||||
return [
|
||||
{ path: envPath, content: envContent },
|
||||
{
|
||||
path: configPath,
|
||||
path: joinConfigPath(configDir, 'config.toml', isWindowsPath),
|
||||
content: configContent,
|
||||
hint: t('keys.useKeyModal.grok.codexConfigTomlHint')
|
||||
},
|
||||
{
|
||||
path: isWindows ? 'PowerShell' : 'Terminal',
|
||||
content: environmentContent
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1318,22 +1454,28 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
}
|
||||
}
|
||||
}
|
||||
// Align context_window with Grok Build official sample (docs.x.ai/build/settings) where known.
|
||||
// Image/video: grok-imagine-image / grok-imagine-video on media endpoints — not this list.
|
||||
const grokModels = {
|
||||
'grok-4.5': {
|
||||
name: 'Grok 4.5',
|
||||
limit: { context: 1000000, output: 128000 }
|
||||
},
|
||||
'grok-4.3': {
|
||||
name: 'Grok 4.3',
|
||||
limit: { context: 1000000, output: 128000 }
|
||||
limit: { context: 1000000, output: 64000 }
|
||||
},
|
||||
'grok-build-0.1': {
|
||||
name: 'Grok Build 0.1',
|
||||
limit: { context: 256000, output: 128000 }
|
||||
limit: { context: 256000, output: 64000 }
|
||||
},
|
||||
'grok-4.20-multi-agent-0309': {
|
||||
name: 'Grok 4.20 Multi Agent (text / web_search)',
|
||||
limit: { context: 1000000, output: 64000 }
|
||||
},
|
||||
'grok-4.3': {
|
||||
name: 'Grok 4.3',
|
||||
limit: { context: 1000000, output: 64000 }
|
||||
},
|
||||
'grok-composer-2.5-fast': {
|
||||
name: 'Grok Composer 2.5 Fast',
|
||||
limit: { context: 500000, output: 128000 }
|
||||
limit: { context: 500000, output: 64000 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,8 +1495,9 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
} else if (platform === 'openai') {
|
||||
provider[platform].models = openaiModels
|
||||
} else if (platform === 'grok') {
|
||||
provider[platform].npm = '@ai-sdk/openai'
|
||||
provider[platform].name = 'Grok'
|
||||
// Custom provider pointing at Sub2API OpenAI-compatible Responses/Chat endpoints.
|
||||
provider[platform].npm = '@ai-sdk/openai-compatible'
|
||||
provider[platform].name = 'Grok via Sub2API'
|
||||
provider[platform].models = grokModels
|
||||
}
|
||||
|
||||
|
||||
@@ -46,14 +46,33 @@ describe('UseKeyModal', () => {
|
||||
)
|
||||
expect(grokTab).toBeDefined()
|
||||
|
||||
const grokConfig = wrapper.findAll('pre code')
|
||||
.map((code) => code.text())
|
||||
.find((content) => content.includes('[model."grok"]'))
|
||||
expect(grokConfig).toBeDefined()
|
||||
expect(grokConfig).toContain('model = "grok-4.5"')
|
||||
expect(grokConfig).toContain('base_url = "https://example.com/v1"')
|
||||
expect(grokConfig).toContain('api_key = "sk-grok-test"')
|
||||
expect(grokConfig).toContain('api_backend = "responses"')
|
||||
const allCode = wrapper.findAll('pre code').map((code) => code.text()).join('\n')
|
||||
expect(allCode).toContain('GROK_MODELS_BASE_URL')
|
||||
expect(allCode).toContain('XAI_API_KEY')
|
||||
expect(allCode).toContain('[model."grok-4.5"]')
|
||||
expect(allCode).toContain('[model."grok-build-0.1"]')
|
||||
expect(allCode).toContain('[model."grok-4.20-multi-agent-0309"]')
|
||||
expect(allCode).toContain('[model."grok-4.3"]')
|
||||
expect(allCode).toContain('default = "grok-4.5"')
|
||||
expect(allCode).toContain('models_base_url = "https://example.com/v1"')
|
||||
expect(allCode).toContain('env_key = "XAI_API_KEY"')
|
||||
expect(allCode).toContain('Keep api_backend = "responses" on every model entry.')
|
||||
expect(allCode).toContain('NOT for image/video generation')
|
||||
expect(allCode).toContain('grok-imagine-image')
|
||||
expect(allCode).toContain('grok-imagine-video')
|
||||
expect(allCode).toMatch(/\[model\."grok-4\.5"\][\s\S]*?context_window = 500000/)
|
||||
expect(allCode).toMatch(/\[model\."grok-build-0\.1"\][\s\S]*?context_window = 256000/)
|
||||
// Prefer env_key; hardcode api_key only as commented alternative
|
||||
expect(allCode).not.toMatch(/^api_key = "sk-grok-test"$/m)
|
||||
|
||||
const modelBlocks = allCode
|
||||
.split(/(?=^\[model\.)/m)
|
||||
.filter((block) => block.startsWith('[model."'))
|
||||
expect(modelBlocks.length).toBeGreaterThanOrEqual(4)
|
||||
for (const block of modelBlocks) {
|
||||
if (block.includes('# [model.')) continue
|
||||
expect(block).toContain('api_backend = "responses"')
|
||||
}
|
||||
|
||||
const windowsTab = wrapper.findAll('button').find(
|
||||
(button) => button.text().trim() === 'Windows'
|
||||
@@ -61,7 +80,7 @@ describe('UseKeyModal', () => {
|
||||
expect(windowsTab).toBeDefined()
|
||||
await windowsTab!.trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.text()).toContain('%userprofile%\\.grok/config.toml')
|
||||
expect(wrapper.text().toLowerCase()).toContain('%userprofile%\\.grok\\config.toml')
|
||||
|
||||
const opencodeTab = wrapper.findAll('button').find((button) =>
|
||||
button.text().includes('keys.useKeyModal.cliTabs.opencode')
|
||||
@@ -71,13 +90,16 @@ describe('UseKeyModal', () => {
|
||||
await nextTick()
|
||||
|
||||
const parsed = JSON.parse(wrapper.find('pre code').text())
|
||||
expect(parsed.provider.grok.npm).toBe('@ai-sdk/openai')
|
||||
expect(parsed.provider.grok.npm).toBe('@ai-sdk/openai-compatible')
|
||||
expect(parsed.provider.grok.name).toBe('Grok via Sub2API')
|
||||
expect(parsed.provider.grok.options).toEqual({
|
||||
baseURL: 'https://example.com/v1',
|
||||
apiKey: 'sk-grok-test'
|
||||
})
|
||||
expect(parsed.provider.grok.models['grok-4.5']).toBeDefined()
|
||||
expect(parsed.provider.grok.models['grok-4.5'].limit.context).toBe(500000)
|
||||
expect(parsed.provider.grok.models['grok-build-0.1']).toBeDefined()
|
||||
expect(parsed.provider.grok.models['grok-4.20-multi-agent-0309']).toBeDefined()
|
||||
expect(parsed.provider.grok.models['grok-composer-2.5-fast']).toBeDefined()
|
||||
expect(parsed.provider.grok.models['gpt-5.6']).toBeUndefined()
|
||||
})
|
||||
@@ -200,22 +222,20 @@ describe('UseKeyModal', () => {
|
||||
await nextTick()
|
||||
|
||||
let codeBlocks = wrapper.findAll('pre code').map((code) => code.text())
|
||||
const configToml = codeBlocks.find((content) => content.includes('[model_providers.sub2api_grok]'))
|
||||
const configToml = codeBlocks.find((content) => content.includes('[model_providers.sub2api]'))
|
||||
expect(configToml).toBeDefined()
|
||||
expect(configToml).toContain('model_provider = "sub2api_grok"')
|
||||
expect(configToml).toContain('model_provider = "sub2api"')
|
||||
expect(configToml).toContain('model = "grok-4.5"')
|
||||
expect(configToml).toContain('model_context_window = 1000000')
|
||||
expect(configToml).toContain('base_url = "https://example.com/v1"')
|
||||
expect(configToml).toContain('env_key = "SUB2API_API_KEY"')
|
||||
expect(configToml).toContain('experimental_bearer_token = "sk-grok-codex-test"')
|
||||
expect(configToml).toContain('wire_api = "responses"')
|
||||
expect(configToml).toContain('supports_websockets = true')
|
||||
expect(configToml).not.toContain('requires_openai_auth')
|
||||
expect(configToml).not.toContain('disable_response_storage')
|
||||
expect(configToml).not.toContain('network_access')
|
||||
expect(configToml).not.toContain('windows_wsl_setup_acknowledged')
|
||||
expect(configToml).toContain('[features]\nresponses_websockets_v2 = true')
|
||||
expect(configToml).not.toContain('goals = true')
|
||||
expect(codeBlocks).toContain('export SUB2API_API_KEY="sk-grok-codex-test"')
|
||||
expect(configToml).toContain('requires_openai_auth = true')
|
||||
expect(configToml).toContain('supports_websockets = false')
|
||||
expect(configToml).toContain('multi-agent / web_search')
|
||||
expect(configToml).toContain('grok-imagine-image')
|
||||
expect(configToml).toContain('grok-imagine-video')
|
||||
expect(configToml).not.toContain('supports_websockets = true')
|
||||
expect(configToml).not.toContain('responses_websockets_v2')
|
||||
expect(wrapper.text()).not.toContain('auth.json')
|
||||
|
||||
const windowsTab = wrapper.findAll('button').find(
|
||||
@@ -226,8 +246,8 @@ describe('UseKeyModal', () => {
|
||||
await nextTick()
|
||||
|
||||
codeBlocks = wrapper.findAll('pre code').map((code) => code.text())
|
||||
expect(wrapper.text()).toContain('%USERPROFILE%\\.codex\\config.toml')
|
||||
expect(codeBlocks).toContain('$env:SUB2API_API_KEY="sk-grok-codex-test"')
|
||||
expect(wrapper.text().toLowerCase()).toContain('%userprofile%\\.codex\\config.toml'.toLowerCase())
|
||||
expect(codeBlocks.join('\n')).toContain('experimental_bearer_token = "sk-grok-codex-test"')
|
||||
})
|
||||
|
||||
it('keeps legacy OpenAI Codex config as the default', () => {
|
||||
|
||||
@@ -169,16 +169,24 @@ export default {
|
||||
note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
|
||||
},
|
||||
grok: {
|
||||
description: 'Configure Grok Build, Claude Code, Codex, or OpenCode to send requests through your Sub2API Grok group.',
|
||||
description:
|
||||
'Configure Grok CLI, Claude Code, Codex, or OpenCode to send requests through your Sub2API Grok group. Text models use Responses; image/video use Imagine model IDs on media endpoints.',
|
||||
claudeDescription: 'Configure Claude Code to send Messages API traffic through your Sub2API Grok group.',
|
||||
codexDescription: 'Configure Codex to send Responses API traffic through your Sub2API Grok group.',
|
||||
configTomlHint: 'Back up an existing config.toml before merging this model entry. Run grok inspect after saving to verify the effective configuration.',
|
||||
codexConfigTomlHint: 'Back up an existing config.toml before merging this provider configuration.',
|
||||
note: 'Save the file as ~/.grok/config.toml, then run grok inspect and select grok from /model.',
|
||||
noteWindows: 'Save the file as %USERPROFILE%\\.grok\\config.toml, then run grok inspect and select grok from /model.',
|
||||
claudeNote: 'Choose one method: run the terminal commands for the current session, or save settings.json for user-level persistent configuration.',
|
||||
codexNote: 'Save config.toml under ~/.codex and set SUB2API_API_KEY before starting Codex.',
|
||||
codexNoteWindows: 'Save config.toml under %USERPROFILE%\\.codex and set SUB2API_API_KEY in PowerShell before starting Codex.',
|
||||
configTomlHint:
|
||||
'Official path: ~/.grok/config.toml (or $GROK_HOME). Prefer env_key = "XAI_API_KEY" over api_key. Every model must set api_backend = "responses" for Sub2API. Back up before merge, then run grok inspect.',
|
||||
codexConfigTomlHint:
|
||||
'Official Codex: wire_api = "responses" only; prefer env_key over experimental_bearer_token; supports_websockets = false for non-OpenAI gateways. Back up ~/.codex/config.toml before merge.',
|
||||
note:
|
||||
'Export GROK_MODELS_BASE_URL and XAI_API_KEY, save config.toml as ~/.grok/config.toml, run grok inspect, then /model grok-4.5 (or grok-build-0.1 for coding).',
|
||||
noteWindows:
|
||||
'Set GROK_MODELS_BASE_URL and XAI_API_KEY, save config.toml as %USERPROFILE%\\.grok\\config.toml, run grok inspect, then /model grok-4.5 (or grok-build-0.1 for coding).',
|
||||
claudeNote:
|
||||
'Choose one method: terminal env for this session, or ~/.claude/settings.json for persistence. Do not commit files that contain your API key.',
|
||||
codexNote:
|
||||
'Export SUB2API_API_KEY, save config.toml under ~/.codex (mkdir -p ~/.codex). Prefer env_key auth; do not commit secrets.',
|
||||
codexNoteWindows:
|
||||
'Set $env:SUB2API_API_KEY, save config.toml under %USERPROFILE%\\.codex. Prefer env_key auth; do not commit secrets.',
|
||||
},
|
||||
opencode: {
|
||||
title: 'OpenCode Example',
|
||||
|
||||
@@ -173,16 +173,24 @@ export default {
|
||||
note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。'
|
||||
},
|
||||
grok: {
|
||||
description: '配置 Grok Build、Claude Code、Codex 或 OpenCode,让请求通过当前 Sub2API Grok 分组发送。',
|
||||
description:
|
||||
'配置 Grok CLI、Claude Code、Codex 或 OpenCode,让请求通过当前 Sub2API Grok 分组发送。文本模型走 Responses;图片/视频使用 Imagine 模型 ID 与媒体端点。',
|
||||
claudeDescription: '配置 Claude Code,让 Messages API 请求通过当前 Sub2API Grok 分组发送。',
|
||||
codexDescription: '配置 Codex,让 Responses API 请求通过当前 Sub2API Grok 分组发送。',
|
||||
configTomlHint: '如已有 config.toml,请先备份再合并此模型配置。保存后运行 grok inspect 验证生效配置。',
|
||||
codexConfigTomlHint: '如已有 config.toml,请先备份再合并此服务商配置。',
|
||||
note: '保存为 ~/.grok/config.toml,然后运行 grok inspect,并在 /model 中选择 grok。',
|
||||
noteWindows: '保存为 %USERPROFILE%\\.grok\\config.toml,然后运行 grok inspect,并在 /model 中选择 grok。',
|
||||
claudeNote: '二选一即可:终端命令仅在当前会话生效;保存 settings.json 可作为用户级持久配置。',
|
||||
codexNote: '将 config.toml 保存到 ~/.codex,并在启动 Codex 前设置 SUB2API_API_KEY。',
|
||||
codexNoteWindows: '将 config.toml 保存到 %USERPROFILE%\\.codex,并在 PowerShell 中设置 SUB2API_API_KEY 后启动 Codex。'
|
||||
configTomlHint:
|
||||
'官方路径:~/.grok/config.toml(或 $GROK_HOME)。优先 env_key = "XAI_API_KEY",勿硬编码 api_key。Sub2API 上每个模型必须 api_backend = "responses"。合并前请备份,保存后运行 grok inspect。',
|
||||
codexConfigTomlHint:
|
||||
'Codex 官方:wire_api 仅支持 "responses";优先 env_key,勿与 experimental_bearer_token 混用;非 OpenAI 网关请 supports_websockets = false。合并前备份 ~/.codex/config.toml。',
|
||||
note:
|
||||
'导出 GROK_MODELS_BASE_URL 与 XAI_API_KEY,将 config.toml 保存为 ~/.grok/config.toml,运行 grok inspect,再用 /model 选择 grok-4.5(编程场景可用 grok-build-0.1)。',
|
||||
noteWindows:
|
||||
'设置 GROK_MODELS_BASE_URL 与 XAI_API_KEY,将 config.toml 保存为 %USERPROFILE%\\.grok\\config.toml,运行 grok inspect,再用 /model 选择 grok-4.5(编程场景可用 grok-build-0.1)。',
|
||||
claudeNote:
|
||||
'二选一:终端环境变量仅当前会话;~/.claude/settings.json 可持久化。请勿把含 API Key 的文件提交到仓库。',
|
||||
codexNote:
|
||||
'导出 SUB2API_API_KEY,将 config.toml 保存到 ~/.codex(可用 mkdir -p ~/.codex)。优先 env_key,勿提交密钥。',
|
||||
codexNoteWindows:
|
||||
'设置 $env:SUB2API_API_KEY,将 config.toml 保存到 %USERPROFILE%\\.codex。优先 env_key,勿提交密钥。'
|
||||
},
|
||||
opencode: {
|
||||
title: 'OpenCode 配置示例',
|
||||
|
||||
Reference in New Issue
Block a user