Skip to content

Commit 961b379

Browse files
committed
feat: add custom providers
1 parent b1cfa84 commit 961b379

11 files changed

Lines changed: 980 additions & 451 deletions

File tree

bun.lock

Lines changed: 212 additions & 85 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "21st-desktop",
3-
"version": "0.0.54",
3+
"version": "0.0.55",
44
"private": true,
55
"description": "1Code - UI for parallel work with AI agents",
66
"author": {
@@ -115,18 +115,19 @@
115115
"devDependencies": {
116116
"@electron-toolkit/preload": "^3.0.1",
117117
"@electron-toolkit/utils": "^4.0.0",
118+
"@electron/rebuild": "^4.0.3",
118119
"@types/better-sqlite3": "^7.6.13",
119120
"@types/diff": "^8.0.0",
120121
"@types/node": "^20.17.50",
121122
"@types/react": "^19.0.7",
122123
"@types/react-dom": "^19.0.3",
124+
"@typescript/native-preview": "^7.0.0-dev.20260204.1",
123125
"@vitejs/plugin-react": "^4.3.4",
124126
"@welldone-software/why-did-you-render": "^10.0.1",
125127
"autoprefixer": "^10.4.20",
126128
"drizzle-kit": "^0.31.8",
127129
"electron": "~39.4.0",
128130
"electron-builder": "^25.1.8",
129-
"@electron/rebuild": "^4.0.3",
130131
"electron-vite": "^3.0.0",
131132
"postcss": "^8.5.1",
132133
"tailwindcss": "^3.4.17",

src/main/lib/trpc/routers/claude.ts

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ function decryptToken(encrypted: string): string {
124124
return safeStorage.decryptString(buffer)
125125
}
126126

127+
function decryptIfNeeded(token: string): string {
128+
if (!token) return token
129+
if (!token.startsWith("enc:")) return token
130+
return decryptToken(token.slice(4))
131+
}
132+
127133
/**
128134
* Get Claude Code OAuth token from local SQLite
129135
* Returns null if not connected
@@ -553,7 +559,7 @@ export const claudeRouter = router({
553559
customConfig: z
554560
.object({
555561
model: z.string().min(1),
556-
token: z.string().min(1),
562+
token: z.string(),
557563
baseUrl: z.string().min(1),
558564
})
559565
.optional(),
@@ -708,15 +714,21 @@ export const claudeRouter = router({
708714
// Use offline config if available
709715
const finalCustomConfig = offlineResult.config || input.customConfig
710716
const isUsingOllama = offlineResult.isUsingOllama
717+
const resolvedCustomConfig = finalCustomConfig
718+
? {
719+
...finalCustomConfig,
720+
token: decryptIfNeeded(finalCustomConfig.token),
721+
}
722+
: undefined
711723

712724
// Track connection method for analytics
713725
let connectionMethod = "claude-subscription" // default (Claude Code OAuth)
714726
if (isUsingOllama) {
715727
connectionMethod = "offline-ollama"
716-
} else if (finalCustomConfig) {
728+
} else if (resolvedCustomConfig) {
717729
// Has custom config = either API key or custom model
718-
const isDefaultAnthropicUrl = !finalCustomConfig.baseUrl ||
719-
finalCustomConfig.baseUrl.includes("anthropic.com")
730+
const isDefaultAnthropicUrl = !resolvedCustomConfig.baseUrl ||
731+
resolvedCustomConfig.baseUrl.includes("anthropic.com")
720732
connectionMethod = isDefaultAnthropicUrl ? "api-key" : "custom-model"
721733
}
722734
setConnectionMethod(connectionMethod)
@@ -824,10 +836,12 @@ export const claudeRouter = router({
824836

825837
// Build full environment for Claude SDK (includes HOME, PATH, etc.)
826838
const claudeEnv = buildClaudeEnv({
827-
...(finalCustomConfig && {
839+
...(resolvedCustomConfig && {
828840
customEnv: {
829-
ANTHROPIC_AUTH_TOKEN: finalCustomConfig.token,
830-
ANTHROPIC_BASE_URL: finalCustomConfig.baseUrl,
841+
...(resolvedCustomConfig.token && {
842+
ANTHROPIC_AUTH_TOKEN: resolvedCustomConfig.token,
843+
}),
844+
ANTHROPIC_BASE_URL: resolvedCustomConfig.baseUrl,
831845
},
832846
}),
833847
enableTasks: input.enableTasks ?? true,
@@ -983,7 +997,14 @@ export const claudeRouter = router({
983997

984998
// Build final env - only add OAuth token if we have one AND no existing API config
985999
// Existing CLI config takes precedence over OAuth
986-
const finalEnv = {
1000+
const finalEnv: {
1001+
[key: string]: string | undefined
1002+
CLAUDE_CODE_OAUTH_TOKEN?: string
1003+
CLAUDE_CONFIG_DIR: string
1004+
ANTHROPIC_BASE_URL?: string
1005+
ANTHROPIC_AUTH_TOKEN?: string
1006+
ANTHROPIC_API_KEY?: string
1007+
} = {
9871008
...claudeEnv,
9881009
...(claudeCodeToken && !hasExistingApiConfig && {
9891010
CLAUDE_CODE_OAUTH_TOKEN: claudeCodeToken,
@@ -1012,38 +1033,40 @@ export const claudeRouter = router({
10121033
console.log(`[claude] ========== END SESSION DEBUG ==========`)
10131034

10141035
console.log(`[SD] Query options - cwd: ${input.cwd}, projectPath: ${input.projectPath || "(not set)"}, mcpServers: ${mcpServersForSdk ? Object.keys(mcpServersForSdk).join(", ") : "(none)"}`)
1015-
if (finalCustomConfig) {
1036+
if (resolvedCustomConfig) {
10161037
const redactedConfig = {
1017-
...finalCustomConfig,
1018-
token: `${finalCustomConfig.token.slice(0, 6)}...`,
1038+
...resolvedCustomConfig,
1039+
token: resolvedCustomConfig.token
1040+
? `${resolvedCustomConfig.token.slice(0, 6)}...`
1041+
: "",
10191042
}
10201043
if (isUsingOllama) {
1021-
console.log(`[Ollama] Using offline mode - Model: ${finalCustomConfig.model}, Base URL: ${finalCustomConfig.baseUrl}`)
1044+
console.log(`[Ollama] Using offline mode - Model: ${resolvedCustomConfig.model}, Base URL: ${resolvedCustomConfig.baseUrl}`)
10221045
} else {
10231046
console.log(`[claude] Custom config: ${JSON.stringify(redactedConfig)}`)
10241047
}
10251048
}
10261049

1027-
const resolvedModel = finalCustomConfig?.model || input.model
1050+
const resolvedModel = resolvedCustomConfig?.model || input.model
10281051

10291052
// DEBUG: If using Ollama, test if it's actually responding
1030-
if (isUsingOllama && finalCustomConfig) {
1053+
if (isUsingOllama && resolvedCustomConfig) {
10311054
console.log('[Ollama Debug] Testing Ollama connectivity...')
10321055
try {
1033-
const testResponse = await fetch(`${finalCustomConfig.baseUrl}/api/tags`, {
1056+
const testResponse = await fetch(`${resolvedCustomConfig.baseUrl}/api/tags`, {
10341057
signal: AbortSignal.timeout(2000)
10351058
})
10361059
if (testResponse.ok) {
10371060
const data = await testResponse.json()
10381061
const models = data.models?.map((m: any) => m.name) || []
10391062
console.log('[Ollama Debug] Ollama is responding. Available models:', models)
10401063

1041-
if (!models.includes(finalCustomConfig.model)) {
1042-
console.error(`[Ollama Debug] WARNING: Model "${finalCustomConfig.model}" not found in Ollama!`)
1064+
if (!models.includes(resolvedCustomConfig.model)) {
1065+
console.error(`[Ollama Debug] WARNING: Model "${resolvedCustomConfig.model}" not found in Ollama!`)
10431066
console.error(`[Ollama Debug] Available models:`, models)
10441067
console.error(`[Ollama Debug] This will likely cause the stream to hang or fail silently.`)
10451068
} else {
1046-
console.log(`[Ollama Debug] ✓ Model "${finalCustomConfig.model}" is available`)
1069+
console.log(`[Ollama Debug] ✓ Model "${resolvedCustomConfig.model}" is available`)
10471070
}
10481071
} else {
10491072
console.error('[Ollama Debug] Ollama returned error:', testResponse.status)
@@ -1468,8 +1491,8 @@ ${prompt}
14681491

14691492
if (isUsingOllama) {
14701493
console.log(`[Ollama] ===== STARTING STREAM ITERATION =====`)
1471-
console.log(`[Ollama] Model: ${finalCustomConfig?.model}`)
1472-
console.log(`[Ollama] Base URL: ${finalCustomConfig?.baseUrl}`)
1494+
console.log(`[Ollama] Model: ${resolvedCustomConfig?.model}`)
1495+
console.log(`[Ollama] Base URL: ${resolvedCustomConfig?.baseUrl}`)
14731496
console.log(`[Ollama] Prompt: "${typeof input.prompt === 'string' ? input.prompt.slice(0, 100) : 'N/A'}..."`)
14741497
console.log(`[Ollama] CWD: ${input.cwd}`)
14751498
}
@@ -1536,7 +1559,7 @@ ${prompt}
15361559
console.error(`[CLAUDE SDK ERROR] CWD: ${input.cwd}`)
15371560
console.error(`[CLAUDE SDK ERROR] Mode: ${input.mode}`)
15381561
console.error(`[CLAUDE SDK ERROR] Session ID: ${msgAny.session_id || 'none'}`)
1539-
console.error(`[CLAUDE SDK ERROR] Has custom config: ${!!finalCustomConfig}`)
1562+
console.error(`[CLAUDE SDK ERROR] Has custom config: ${!!resolvedCustomConfig}`)
15401563
console.error(`[CLAUDE SDK ERROR] Is using Ollama: ${isUsingOllama}`)
15411564
console.error(`[CLAUDE SDK ERROR] Model: ${resolvedModel || 'default'}`)
15421565
console.error(`[CLAUDE SDK ERROR] Has OAuth token: ${!!claudeCodeToken}`)
@@ -1794,7 +1817,7 @@ ${prompt}
17941817
console.error(`[Ollama] 2. Model failed to start generating (check Ollama logs: ollama logs)`)
17951818
console.error(`[Ollama] 3. Network issue between Claude SDK and Ollama`)
17961819
console.error(`[Ollama] ===== NEXT STEPS =====`)
1797-
console.error(`[Ollama] 1. Check if model works: curl http://localhost:11434/api/generate -d '{"model":"${finalCustomConfig?.model}","prompt":"test"}'`)
1820+
console.error(`[Ollama] 1. Check if model works: curl http://localhost:11434/api/generate -d '{"model":"${resolvedCustomConfig?.model}","prompt":"test"}'`)
17981821
console.error(`[Ollama] 2. Check Ollama version supports Messages API`)
17991822
console.error(`[Ollama] 3. Try using a proxy that converts Anthropic API → Ollama format`)
18001823
}
@@ -2377,6 +2400,64 @@ ${prompt}
23772400
return { success: true }
23782401
}),
23792402

2403+
fetchModels: publicProcedure
2404+
.input(
2405+
z.object({
2406+
baseUrl: z.string().min(1),
2407+
token: z.string().optional(),
2408+
}),
2409+
)
2410+
.mutation(async ({ input }) => {
2411+
const cleanUrl = input.baseUrl.replace(/\/$/, "")
2412+
const authToken = input.token ? decryptIfNeeded(input.token) : ""
2413+
2414+
try {
2415+
const ollamaRes = await fetch(`${cleanUrl}/api/tags`)
2416+
if (ollamaRes.ok) {
2417+
const data = await ollamaRes.json()
2418+
if (Array.isArray(data.models)) {
2419+
const models = data.models
2420+
.map((model: { name?: string }) => model.name)
2421+
.filter((name: string | undefined): name is string => Boolean(name))
2422+
return { models }
2423+
}
2424+
}
2425+
} catch (error) {
2426+
console.warn("[models] Failed to fetch Ollama tags:", error)
2427+
}
2428+
2429+
try {
2430+
const res = await fetch(`${cleanUrl}/v1/models`, {
2431+
headers: authToken ? { Authorization: `Bearer ${authToken}` } : {},
2432+
})
2433+
if (res.ok) {
2434+
const data = await res.json()
2435+
if (Array.isArray(data.data)) {
2436+
const models = data.data
2437+
.map((model: { id?: string }) => model.id)
2438+
.filter((id: string | undefined): id is string => Boolean(id))
2439+
return { models }
2440+
}
2441+
}
2442+
} catch (error) {
2443+
console.warn("[models] Failed to fetch OpenAI-compatible models:", error)
2444+
}
2445+
2446+
return { models: [] as string[] }
2447+
}),
2448+
2449+
encryptToken: publicProcedure
2450+
.input(
2451+
z.object({
2452+
token: z.string().min(1),
2453+
}),
2454+
)
2455+
.mutation(({ input }) => {
2456+
const encrypted = safeStorage.isEncryptionAvailable()
2457+
? safeStorage.encryptString(input.token).toString("base64")
2458+
: Buffer.from(input.token, "utf-8").toString("base64")
2459+
return { encrypted: `enc:${encrypted}` }
2460+
}),
23802461
getPendingPluginMcpApprovals: publicProcedure
23812462
.input(z.object({ projectPath: z.string().optional() }))
23822463
.query(async ({ input }) => {

0 commit comments

Comments
 (0)