From afaa2253deac70d57771317eba010c7e06622ea1 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Wed, 25 Mar 2026 01:15:37 +0100 Subject: [PATCH] feat(electrobun): auto-detect Claude models via OAuth token from CLI credentials --- ui-electrobun/src/bun/model-fetcher.ts | 33 ++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/ui-electrobun/src/bun/model-fetcher.ts b/ui-electrobun/src/bun/model-fetcher.ts index 58e9aab..3e50ec0 100644 --- a/ui-electrobun/src/bun/model-fetcher.ts +++ b/ui-electrobun/src/bun/model-fetcher.ts @@ -13,18 +13,41 @@ export interface ModelInfo { const TIMEOUT = 8000; -// Known Claude models as fallback when API key is not available +// Known Claude models as ultimate fallback const KNOWN_CLAUDE_MODELS: ModelInfo[] = [ { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'claude' }, { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'claude' }, - { id: 'claude-opus-4-5-20250514', name: 'Claude Opus 4.5', provider: 'claude' }, + { id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5', provider: 'claude' }, { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'claude' }, { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: 'claude' }, - { id: 'claude-sonnet-3-7-20250219', name: 'Claude Sonnet 3.7', provider: 'claude' }, ]; +/** + * Try to get an API key for Claude: + * 1. ANTHROPIC_API_KEY env var (explicit) + * 2. Claude CLI OAuth token from ~/.claude/.credentials.json (auto-detected) + */ +function getClaudeApiKey(): string | null { + if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY; + try { + const fs = require('fs'); + const path = require('path'); + const home = process.env.HOME || '/home'; + const credPath = path.join(home, '.claude', '.credentials.json'); + const data = JSON.parse(fs.readFileSync(credPath, 'utf8')); + const token = data?.claudeAiOauth?.accessToken; + if (token && typeof token === 'string' && token.startsWith('sk-ant-')) { + // Check if token is expired + const expiresAt = data.claudeAiOauth.expiresAt; + if (expiresAt && Date.now() > expiresAt) return null; + return token; + } + } catch { /* credentials file not found or unreadable */ } + return null; +} + export async function fetchClaudeModels(): Promise { - const apiKey = process.env.ANTHROPIC_API_KEY; + const apiKey = getClaudeApiKey(); if (!apiKey) return KNOWN_CLAUDE_MODELS; try { const res = await fetch('https://api.anthropic.com/v1/models?limit=100', { @@ -38,7 +61,7 @@ export async function fetchClaudeModels(): Promise { const data = await res.json() as { data?: Array<{ id: string; display_name?: string }> }; const live = (data.data ?? []) .map(m => ({ id: m.id, name: m.display_name ?? m.id, provider: 'claude' })) - .sort((a, b) => a.id.localeCompare(b.id)); + .sort((a, b) => b.id.localeCompare(a.id)); // newest first return live.length > 0 ? live : KNOWN_CLAUDE_MODELS; } catch { return KNOWN_CLAUDE_MODELS;