From f2cc29f5d7129dd8069a12ebe03e2f2ab309060a Mon Sep 17 00:00:00 2001 From: wusumac <736139669@qq.com> Date: Sun, 2 Aug 2026 22:32:19 +0800 Subject: [PATCH] feat(hub,web): support custom Claude models via settings.json (customClaudeModels) --- hub/src/config/settings.ts | 2 ++ hub/src/startHub.ts | 3 ++- hub/src/web/routes/claudeModels.ts | 23 ++++++++++++++++++ hub/src/web/server.ts | 5 ++++ web/src/api/client.ts | 4 ++++ web/src/components/NewSession/index.tsx | 31 ++++++++++++++++++++++++- 6 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 hub/src/web/routes/claudeModels.ts diff --git a/hub/src/config/settings.ts b/hub/src/config/settings.ts index 0c2be780..b39cfbea 100644 --- a/hub/src/config/settings.ts +++ b/hub/src/config/settings.ts @@ -22,6 +22,8 @@ export interface Settings { corsOrigins?: string[] /** Per-hub relay auth key issued by the relay server (/issue) */ relayAuthKey?: string + /** Custom model names offered in the Claude model picker (e.g. DeepSeek via ANTHROPIC_BASE_URL). */ + customClaudeModels?: string[] } export function getSettingsFile(dataDir: string): string { diff --git a/hub/src/startHub.ts b/hub/src/startHub.ts index 4dd29fc1..13cfee80 100644 --- a/hub/src/startHub.ts +++ b/hub/src/startHub.ts @@ -259,7 +259,8 @@ export async function startHub(options: StartHubOptions = {}): Promise { + const app = new Hono() + + app.get('/claude/custom-models', async (c) => { + const settings = await readSettings(getSettingsFile(dataDir)) + const models = Array.isArray(settings?.customClaudeModels) + ? settings.customClaudeModels + : [] + return c.json({ models }) + }) + + return app +} diff --git a/hub/src/web/server.ts b/hub/src/web/server.ts index 6ae596b5..2021d475 100644 --- a/hub/src/web/server.ts +++ b/hub/src/web/server.ts @@ -26,6 +26,7 @@ import { createCodexDesktopRoutes } from './routes/codexDesktop' import { createPushRoutes } from './routes/push' import { createDevicesRoutes } from './routes/devices' import { createVoiceRoutes } from './routes/voice' +import { createClaudeModelsRoutes } from './routes/claudeModels' import type { SSEManager } from '../sse/sseManager' import type { VisibilityTracker } from '../visibility/visibilityTracker' import type { Server as BunServer, ServerWebSocket } from 'bun' @@ -220,6 +221,7 @@ function createWebApp(options: { embeddedAssetMap: Map | null relayMode?: boolean officialWebUrl?: string + dataDir: string }): Hono { const app = new Hono() @@ -259,6 +261,7 @@ function createWebApp(options: { getSyncEngine: options.getSyncEngine })) app.route('/api', createPushRoutes(options.store, options.vapidPublicKey)) + app.route('/api', createClaudeModelsRoutes(options.dataDir)) app.route('/api', createDevicesRoutes(options.store)) app.route('/api', createVoiceRoutes()) @@ -376,6 +379,7 @@ export async function startWebServer(options: { corsOrigins?: string[] relayMode?: boolean officialWebUrl?: string + dataDir: string }): Promise> { const isCompiled = isBunCompiled() const embeddedAssetMap = isCompiled ? await loadEmbeddedAssetMap() : null @@ -388,6 +392,7 @@ export async function startWebServer(options: { vapidPublicKey: options.vapidPublicKey, corsOrigins: options.corsOrigins, embeddedAssetMap, + dataDir: options.dataDir, relayMode: options.relayMode, officialWebUrl: options.officialWebUrl }) diff --git a/web/src/api/client.ts b/web/src/api/client.ts index ec3e5706..6958a411 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -203,6 +203,10 @@ export class ApiClient { return await this.request('/api/push/vapid-public-key') } + async getClaudeCustomModels(): Promise<{ models: string[] }> { + return await this.request<{ models: string[] }>('/api/claude/custom-models') + } + async subscribePushNotifications(payload: PushSubscriptionPayload): Promise { await this.request('/api/push/subscribe', { method: 'POST', diff --git a/web/src/components/NewSession/index.tsx b/web/src/components/NewSession/index.tsx index a6500215..c6c5953f 100644 --- a/web/src/components/NewSession/index.tsx +++ b/web/src/components/NewSession/index.tsx @@ -37,6 +37,7 @@ import { shouldRestoreNewSessionFormDraft } from './newSessionFormDraft' import type { AgentType, LaunchEffort, CodexReasoningEffort, NewSessionServiceTier, SessionType } from './types' +import { MODEL_OPTIONS } from './types' import { ActionButtons } from './ActionButtons' import { AgentSelector } from './AgentSelector' import { CollaborationModeSelector } from './CollaborationModeSelector' @@ -262,6 +263,32 @@ export function NewSession(props: { enabled: agent === 'codex' && Boolean(machineId) }) const [agySelectedModel, setAgySelectedModel] = useState(null) + const [claudeCustomModels, setClaudeCustomModels] = useState([]) + useEffect(() => { + let cancelled = false + if (!props.api) { + return + } + props.api.getClaudeCustomModels().then((result) => { + if (!cancelled) { + setClaudeCustomModels(Array.isArray(result.models) ? result.models : []) + } + }).catch(() => { + // Custom models are optional — fall back to the built-in presets. + }) + return () => { + cancelled = true + } + }, [props.api]) + const claudeModelOptions = useMemo(() => { + const options = [...MODEL_OPTIONS.claude] + for (const modelName of claudeCustomModels) { + if (!options.some((option) => option.value === modelName)) { + options.push({ value: modelName, label: modelName }) + } + } + return options + }, [claudeCustomModels]) const runnerSpawnError = useMemo( () => formatRunnerSpawnError(selectedMachine), [selectedMachine] @@ -1458,7 +1485,9 @@ export function NewSession(props: { agent={agent} model={model} options={ - agent === 'codex' + agent === 'claude' + ? claudeModelOptions + : agent === 'codex' ? codexModelOptions : agent === 'grok' ? grokModelOptions