fix: model not display life-free model
Browse files- src/components/OpenCodeLoginFlow.tsx +36 -192
- src/utils/model/model.ts +2 -2
- src/utils/model/modelOptions.ts +19 -8
src/components/OpenCodeLoginFlow.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import * as React from 'react'
|
| 2 |
import { useState, useEffect } from 'react'
|
| 3 |
-
import { saveOpenCodeApiKey, getOpenCodeApiKey
|
| 4 |
import { Box, Text } from '../ink.js'
|
| 5 |
import TextInput from './TextInput.js'
|
| 6 |
import { Select } from './CustomSelect/select.js'
|
|
@@ -10,13 +10,7 @@ type OpenCodeLoginFlowProps = {
|
|
| 10 |
startingMessage?: string
|
| 11 |
}
|
| 12 |
|
| 13 |
-
type LoginMode = 'menu' | '
|
| 14 |
-
|
| 15 |
-
type OpencodeModel = {
|
| 16 |
-
label: string
|
| 17 |
-
value: string
|
| 18 |
-
description: string
|
| 19 |
-
}
|
| 20 |
|
| 21 |
export function OpenCodeLoginFlow({
|
| 22 |
onDone,
|
|
@@ -24,125 +18,23 @@ export function OpenCodeLoginFlow({
|
|
| 24 |
}: OpenCodeLoginFlowProps): React.ReactNode {
|
| 25 |
const [mode, setMode] = useState<LoginMode>('menu')
|
| 26 |
const [isBusy, setIsBusy] = useState(false)
|
| 27 |
-
const [isLoadingModels, setIsLoadingModels] = useState(false)
|
| 28 |
const [status, setStatus] = useState<string | null>(null)
|
| 29 |
const [inputValue, setInputValue] = useState('')
|
| 30 |
const [cursorOffset, setCursorOffset] = useState(0)
|
| 31 |
-
const [selectedModel, setSelectedModel] = useState('')
|
| 32 |
const [existingApiKey, setExistingApiKey] = useState<string | null>(null)
|
| 33 |
-
const [existingModelName, setExistingModelName] = useState<string | null>(null)
|
| 34 |
-
const [availableModels, setAvailableModels] = useState<OpencodeModel[]>([])
|
| 35 |
|
| 36 |
useEffect(() => {
|
| 37 |
const key = getOpenCodeApiKey()
|
| 38 |
if (key) {
|
| 39 |
setExistingApiKey(key)
|
| 40 |
}
|
| 41 |
-
const model = getOpenCodeModelName()
|
| 42 |
-
if (model) {
|
| 43 |
-
setExistingModelName(model)
|
| 44 |
-
setSelectedModel(model)
|
| 45 |
-
}
|
| 46 |
}, [])
|
| 47 |
|
| 48 |
-
|
| 49 |
-
setIsLoadingModels(true)
|
| 50 |
-
try {
|
| 51 |
-
// Use native https module for better reliability in bundled environment
|
| 52 |
-
const https = await import('https')
|
| 53 |
-
const data = await new Promise<string>((resolve, reject) => {
|
| 54 |
-
const req = https.get('https://opencode.ai/zen/v1/models', {
|
| 55 |
-
headers: {
|
| 56 |
-
'User-Agent': 'claude-code/2.1.88',
|
| 57 |
-
},
|
| 58 |
-
timeout: 15000,
|
| 59 |
-
}, res => {
|
| 60 |
-
let body = ''
|
| 61 |
-
res.on('data', chunk => { body += chunk })
|
| 62 |
-
res.on('end', () => resolve(body))
|
| 63 |
-
res.on('error', reject)
|
| 64 |
-
})
|
| 65 |
-
req.on('error', reject)
|
| 66 |
-
req.on('timeout', () => {
|
| 67 |
-
req.destroy()
|
| 68 |
-
reject(new Error('Request timed out'))
|
| 69 |
-
})
|
| 70 |
-
})
|
| 71 |
-
|
| 72 |
-
const parsed = JSON.parse(data) as { data?: Array<{ id: string }> }
|
| 73 |
-
if (Array.isArray(parsed.data) && parsed.data.length > 0) {
|
| 74 |
-
const models = parsed.data.map(m => ({
|
| 75 |
-
label: m.id,
|
| 76 |
-
value: m.id,
|
| 77 |
-
description: m.id.endsWith('-free') ? '限时免费模型' : 'OpenCode Zen 模型',
|
| 78 |
-
}))
|
| 79 |
-
setAvailableModels(models)
|
| 80 |
-
if (!selectedModel && models.length > 0) {
|
| 81 |
-
setSelectedModel(models[0].value)
|
| 82 |
-
}
|
| 83 |
-
} else {
|
| 84 |
-
setStatus('API returned empty model list')
|
| 85 |
-
}
|
| 86 |
-
} catch (err) {
|
| 87 |
-
console.error('OpenCode models fetch error:', err)
|
| 88 |
-
setStatus(`Failed to fetch models: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
| 89 |
-
} finally {
|
| 90 |
-
setIsLoadingModels(false)
|
| 91 |
-
}
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
const menuOptions = [
|
| 95 |
-
{
|
| 96 |
-
label: (
|
| 97 |
-
<Text>
|
| 98 |
-
Use free models{' '}
|
| 99 |
-
<Text dimColor={true}>No API key required, select a model</Text>
|
| 100 |
-
{'\n'}
|
| 101 |
-
</Text>
|
| 102 |
-
),
|
| 103 |
-
value: 'free_models',
|
| 104 |
-
},
|
| 105 |
-
{
|
| 106 |
-
label: (
|
| 107 |
-
<Text>
|
| 108 |
-
Paste OpenCode Zen API key{' '}
|
| 109 |
-
<Text dimColor={true}>Access paid models, pay-as-you-go</Text>
|
| 110 |
-
{'\n'}
|
| 111 |
-
</Text>
|
| 112 |
-
),
|
| 113 |
-
value: 'api_key',
|
| 114 |
-
},
|
| 115 |
-
] as const
|
| 116 |
-
|
| 117 |
-
async function handleMenuSelection(value: string): Promise<void> {
|
| 118 |
-
setStatus(null)
|
| 119 |
-
|
| 120 |
-
if (value === 'api_key') {
|
| 121 |
-
setInputValue('')
|
| 122 |
-
setCursorOffset(0)
|
| 123 |
-
setMode('api_key')
|
| 124 |
-
return
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
if (value === 'free_models') {
|
| 128 |
-
setMode('free_models')
|
| 129 |
-
// Fetch models when entering free models mode
|
| 130 |
-
if (availableModels.length === 0) {
|
| 131 |
-
await fetchModels()
|
| 132 |
-
}
|
| 133 |
-
return
|
| 134 |
-
}
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
async function handleFreeModelSelect(modelValue?: string): Promise<void> {
|
| 138 |
-
const modelToSave = modelValue || selectedModel
|
| 139 |
-
if (!modelToSave) return
|
| 140 |
-
|
| 141 |
setIsBusy(true)
|
| 142 |
setStatus(null)
|
| 143 |
try {
|
| 144 |
-
|
| 145 |
-
await saveOpenCodeApiKey('', modelToSave)
|
| 146 |
onDone()
|
| 147 |
} catch (error) {
|
| 148 |
setStatus(error instanceof Error ? error.message : String(error))
|
|
@@ -162,7 +54,7 @@ export function OpenCodeLoginFlow({
|
|
| 162 |
setIsBusy(true)
|
| 163 |
setStatus(null)
|
| 164 |
try {
|
| 165 |
-
await saveOpenCodeApiKey(keyToSave)
|
| 166 |
onDone()
|
| 167 |
} catch (error) {
|
| 168 |
setStatus(error instanceof Error ? error.message : String(error))
|
|
@@ -175,81 +67,6 @@ export function OpenCodeLoginFlow({
|
|
| 175 |
return (
|
| 176 |
<Box flexDirection="column" gap={1}>
|
| 177 |
<Text>Configuring OpenCode Zen for Better-Clawd...</Text>
|
| 178 |
-
<Text dimColor={true}>
|
| 179 |
-
OpenCode Zen provides free models with no API key required.
|
| 180 |
-
</Text>
|
| 181 |
-
</Box>
|
| 182 |
-
)
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
if (mode === 'free_models') {
|
| 186 |
-
if (isLoadingModels) {
|
| 187 |
-
return (
|
| 188 |
-
<Box flexDirection="column" gap={1}>
|
| 189 |
-
<Text>Loading available models...</Text>
|
| 190 |
-
</Box>
|
| 191 |
-
)
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
if (availableModels.length === 0) {
|
| 195 |
-
return (
|
| 196 |
-
<Box flexDirection="column" gap={1}>
|
| 197 |
-
<Text bold={true}>No models available</Text>
|
| 198 |
-
<Text dimColor={true}>
|
| 199 |
-
{status || 'Failed to fetch models from OpenCode Zen API.'}
|
| 200 |
-
</Text>
|
| 201 |
-
<Box flexDirection="row" gap={2} marginTop={1}>
|
| 202 |
-
<Text color="subtle">Esc to go back</Text>
|
| 203 |
-
</Box>
|
| 204 |
-
</Box>
|
| 205 |
-
)
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
return (
|
| 209 |
-
<Box flexDirection="column" gap={1}>
|
| 210 |
-
<Text bold={true}>
|
| 211 |
-
{startingMessage ?? 'Select a free model from OpenCode Zen.'}
|
| 212 |
-
</Text>
|
| 213 |
-
<Text dimColor={true}>
|
| 214 |
-
Free models are available from OpenCode Zen API.
|
| 215 |
-
</Text>
|
| 216 |
-
<Text>Select model:</Text>
|
| 217 |
-
<Select
|
| 218 |
-
options={availableModels.map(model => ({
|
| 219 |
-
label: (
|
| 220 |
-
<Text>
|
| 221 |
-
{model.label}{' '}
|
| 222 |
-
<Text dimColor={true}>{model.description}</Text>
|
| 223 |
-
{'\n'}
|
| 224 |
-
</Text>
|
| 225 |
-
),
|
| 226 |
-
value: model.value,
|
| 227 |
-
}))}
|
| 228 |
-
onChange={value => {
|
| 229 |
-
setSelectedModel(value)
|
| 230 |
-
// 选择后自动提交,直接传递 value 避免闭包问题
|
| 231 |
-
void handleFreeModelSelect(value)
|
| 232 |
-
}}
|
| 233 |
-
/>
|
| 234 |
-
<Box flexDirection="row" gap={2} marginTop={1}>
|
| 235 |
-
<Text color="subtle">Select a model to continue</Text>
|
| 236 |
-
<Text color="subtle">·</Text>
|
| 237 |
-
<Text color="subtle">Esc to cancel</Text>
|
| 238 |
-
</Box>
|
| 239 |
-
{status ? <Text color="error">{status}</Text> : null}
|
| 240 |
-
<Box marginTop={1}>
|
| 241 |
-
<TextInput
|
| 242 |
-
value=""
|
| 243 |
-
onChange={() => {}}
|
| 244 |
-
onSubmit={handleFreeModelSelect}
|
| 245 |
-
onExit={() => setMode('menu')}
|
| 246 |
-
cursorOffset={0}
|
| 247 |
-
onChangeCursorOffset={() => {}}
|
| 248 |
-
columns={72}
|
| 249 |
-
focus={true}
|
| 250 |
-
placeholder="Or press Enter to use selected model"
|
| 251 |
-
/>
|
| 252 |
-
</Box>
|
| 253 |
</Box>
|
| 254 |
)
|
| 255 |
}
|
|
@@ -297,16 +114,43 @@ export function OpenCodeLoginFlow({
|
|
| 297 |
'Better-Clawd can use OpenCode Zen free models or with a Zen API key.'}
|
| 298 |
</Text>
|
| 299 |
<Text dimColor={true}>
|
| 300 |
-
|
| 301 |
{'\n'}
|
| 302 |
-
|
| 303 |
</Text>
|
| 304 |
{status ? <Text color="error">{status}</Text> : null}
|
| 305 |
<Box>
|
| 306 |
<Select
|
| 307 |
-
options={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
onChange={value => {
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
}}
|
| 311 |
/>
|
| 312 |
</Box>
|
|
|
|
| 1 |
import * as React from 'react'
|
| 2 |
import { useState, useEffect } from 'react'
|
| 3 |
+
import { saveOpenCodeApiKey, getOpenCodeApiKey } from '../utils/auth.js'
|
| 4 |
import { Box, Text } from '../ink.js'
|
| 5 |
import TextInput from './TextInput.js'
|
| 6 |
import { Select } from './CustomSelect/select.js'
|
|
|
|
| 10 |
startingMessage?: string
|
| 11 |
}
|
| 12 |
|
| 13 |
+
type LoginMode = 'menu' | 'api_key'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
export function OpenCodeLoginFlow({
|
| 16 |
onDone,
|
|
|
|
| 18 |
}: OpenCodeLoginFlowProps): React.ReactNode {
|
| 19 |
const [mode, setMode] = useState<LoginMode>('menu')
|
| 20 |
const [isBusy, setIsBusy] = useState(false)
|
|
|
|
| 21 |
const [status, setStatus] = useState<string | null>(null)
|
| 22 |
const [inputValue, setInputValue] = useState('')
|
| 23 |
const [cursorOffset, setCursorOffset] = useState(0)
|
|
|
|
| 24 |
const [existingApiKey, setExistingApiKey] = useState<string | null>(null)
|
|
|
|
|
|
|
| 25 |
|
| 26 |
useEffect(() => {
|
| 27 |
const key = getOpenCodeApiKey()
|
| 28 |
if (key) {
|
| 29 |
setExistingApiKey(key)
|
| 30 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}, [])
|
| 32 |
|
| 33 |
+
async function handleFreeModels(): Promise<void> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
setIsBusy(true)
|
| 35 |
setStatus(null)
|
| 36 |
try {
|
| 37 |
+
await saveOpenCodeApiKey('', '')
|
|
|
|
| 38 |
onDone()
|
| 39 |
} catch (error) {
|
| 40 |
setStatus(error instanceof Error ? error.message : String(error))
|
|
|
|
| 54 |
setIsBusy(true)
|
| 55 |
setStatus(null)
|
| 56 |
try {
|
| 57 |
+
await saveOpenCodeApiKey(keyToSave, '')
|
| 58 |
onDone()
|
| 59 |
} catch (error) {
|
| 60 |
setStatus(error instanceof Error ? error.message : String(error))
|
|
|
|
| 67 |
return (
|
| 68 |
<Box flexDirection="column" gap={1}>
|
| 69 |
<Text>Configuring OpenCode Zen for Better-Clawd...</Text>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
</Box>
|
| 71 |
)
|
| 72 |
}
|
|
|
|
| 114 |
'Better-Clawd can use OpenCode Zen free models or with a Zen API key.'}
|
| 115 |
</Text>
|
| 116 |
<Text dimColor={true}>
|
| 117 |
+
Free models require no API key. Use an API key to access paid models.
|
| 118 |
{'\n'}
|
| 119 |
+
After login, use /model to pick a specific model.
|
| 120 |
</Text>
|
| 121 |
{status ? <Text color="error">{status}</Text> : null}
|
| 122 |
<Box>
|
| 123 |
<Select
|
| 124 |
+
options={[
|
| 125 |
+
{
|
| 126 |
+
label: (
|
| 127 |
+
<Text>
|
| 128 |
+
Use free models{' '}
|
| 129 |
+
<Text dimColor={true}>No API key required</Text>
|
| 130 |
+
{'\n'}
|
| 131 |
+
</Text>
|
| 132 |
+
),
|
| 133 |
+
value: 'free_models',
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
label: (
|
| 137 |
+
<Text>
|
| 138 |
+
Paste OpenCode Zen API key{' '}
|
| 139 |
+
<Text dimColor={true}>Access paid models, pay-as-you-go</Text>
|
| 140 |
+
{'\n'}
|
| 141 |
+
</Text>
|
| 142 |
+
),
|
| 143 |
+
value: 'api_key',
|
| 144 |
+
},
|
| 145 |
+
]}
|
| 146 |
onChange={value => {
|
| 147 |
+
if (value === 'free_models') {
|
| 148 |
+
void handleFreeModels()
|
| 149 |
+
} else {
|
| 150 |
+
setInputValue('')
|
| 151 |
+
setCursorOffset(0)
|
| 152 |
+
setMode('api_key')
|
| 153 |
+
}
|
| 154 |
}}
|
| 155 |
/>
|
| 156 |
</Box>
|
src/utils/model/model.ts
CHANGED
|
@@ -262,7 +262,7 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
|
| 262 |
authProvider?: string
|
| 263 |
openCodeModelName?: string
|
| 264 |
}
|
| 265 |
-
isOpenCodeProvider = config.authProvider === 'opencode'
|
| 266 |
} catch {
|
| 267 |
// Ignore errors, fall through to default behavior
|
| 268 |
}
|
|
@@ -273,7 +273,7 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
|
| 273 |
if (openCodeModelName) {
|
| 274 |
return openCodeModelName
|
| 275 |
}
|
| 276 |
-
// Fallback to
|
| 277 |
return 'big-pickle'
|
| 278 |
}
|
| 279 |
|
|
|
|
| 262 |
authProvider?: string
|
| 263 |
openCodeModelName?: string
|
| 264 |
}
|
| 265 |
+
isOpenCodeProvider = config.authProvider === 'opencode'
|
| 266 |
} catch {
|
| 267 |
// Ignore errors, fall through to default behavior
|
| 268 |
}
|
|
|
|
| 273 |
if (openCodeModelName) {
|
| 274 |
return openCodeModelName
|
| 275 |
}
|
| 276 |
+
// Fallback to big-pickle for free models
|
| 277 |
return 'big-pickle'
|
| 278 |
}
|
| 279 |
|
src/utils/model/modelOptions.ts
CHANGED
|
@@ -491,20 +491,31 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
|
|
| 491 |
}
|
| 492 |
|
| 493 |
// OpenCode Zen: Fetch models dynamically from API
|
|
|
|
|
|
|
| 494 |
if (getAPIProvider() === 'opencode') {
|
| 495 |
const defaultOpt = getDefaultOptionForUser(fastMode)
|
| 496 |
try {
|
| 497 |
const { getCachedOpencodeModels } = require('../../services/api/opencodeClient.js')
|
|
|
|
| 498 |
const models = getCachedOpencodeModels()
|
|
|
|
|
|
|
| 499 |
if (models && Array.isArray(models) && models.length > 0) {
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
}
|
| 509 |
} catch {
|
| 510 |
// Ignore errors
|
|
|
|
| 491 |
}
|
| 492 |
|
| 493 |
// OpenCode Zen: Fetch models dynamically from API
|
| 494 |
+
// If no API key, show only free models (ending with -free)
|
| 495 |
+
// If API key is provided, show all models except free ones
|
| 496 |
if (getAPIProvider() === 'opencode') {
|
| 497 |
const defaultOpt = getDefaultOptionForUser(fastMode)
|
| 498 |
try {
|
| 499 |
const { getCachedOpencodeModels } = require('../../services/api/opencodeClient.js')
|
| 500 |
+
const { getOpenCodeApiKey: getApiKey } = require('../../utils/auth.js')
|
| 501 |
const models = getCachedOpencodeModels()
|
| 502 |
+
const hasApiKey = !!getApiKey()
|
| 503 |
+
|
| 504 |
if (models && Array.isArray(models) && models.length > 0) {
|
| 505 |
+
const filtered = hasApiKey
|
| 506 |
+
? models.filter(m => !m.id.endsWith('-free'))
|
| 507 |
+
: models.filter(m => m.id.endsWith('-free'))
|
| 508 |
+
|
| 509 |
+
if (filtered.length > 0) {
|
| 510 |
+
return [
|
| 511 |
+
defaultOpt,
|
| 512 |
+
...filtered.map(m => ({
|
| 513 |
+
value: m.id,
|
| 514 |
+
label: m.name || m.id,
|
| 515 |
+
description: hasApiKey ? 'OpenCode Zen Model' : '限时免费模型',
|
| 516 |
+
})),
|
| 517 |
+
]
|
| 518 |
+
}
|
| 519 |
}
|
| 520 |
} catch {
|
| 521 |
// Ignore errors
|