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:
IanShaw027
2026-08-08 10:26:16 +08:00
parent 2e857b20b8
commit f3bac4619e
25 changed files with 323 additions and 155 deletions
@@ -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' })
+193 -50
View File
@@ -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', () => {