@@ -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