Skip to content

Commit ea04009

Browse files
committed
feat: implement dynamic model fetching and update default model in Guardian panel
1 parent 07f5ee1 commit ea04009

File tree

5 files changed

+67
-16
lines changed

5 files changed

+67
-16
lines changed

src/lib/components/panels/GuardianPanel.svelte

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { aiAnalysisHelpers } from '$lib/stores/ai-analysis.js';
66
import { loadThemePreset, themePresets } from '$lib/stores/theme.js';
77
import { uiHelpers } from '$lib/stores/ui';
8+
import { fetchOpenRouterModels } from '$lib/utils/ai-analysis.js';
89
910
let apiKeyInput = '';
1011
let guardianName = '';
@@ -23,28 +24,29 @@
2324
2425
const displayKey = (k: keyof typeof themePresets) => (k === 'tokyoNight' ? 'tokyo-night' : k);
2526
26-
// OpenRouter model selection
27-
const models = [
28-
'anthropic/claude-3.5-sonnet',
29-
'openai/gpt-4o-mini',
30-
'google/gemini-1.5-pro',
31-
'mistralai/mixtral-8x7b-instruct',
32-
] as const;
27+
// OpenRouter model selection (dynamic; fallback list shown until fetched)
28+
let modelList: string[] = [
29+
'openai/gpt-oss-20b:free',
30+
'mistralai/mixtral-8x7b-instruct:free',
31+
'google/gemma-2-9b-it:free',
32+
'openchat/openchat-7b:free',
33+
];
3334
let currentModelIndex = 0;
35+
let modelsLoading = false;
3436
3537
function loadModelFromStorage() {
3638
const saved = guardianHelpers.load();
3739
const savedModel = saved?.model as string | undefined;
3840
if (savedModel) {
39-
const idx = models.indexOf(savedModel as any);
41+
const idx = modelList.indexOf(savedModel);
4042
currentModelIndex = idx >= 0 ? idx : 0;
4143
} else {
4244
currentModelIndex = 0;
4345
}
4446
}
4547
4648
function displayModel() {
47-
return models[currentModelIndex] || models[0];
49+
return modelList[currentModelIndex] || modelList[0];
4850
}
4951
5052
function persistModel() {
@@ -53,13 +55,36 @@
5355
5456
function cycleModel(dir: 'prev' | 'next') {
5557
if (dir === 'next') {
56-
currentModelIndex = (currentModelIndex + 1) % models.length;
58+
currentModelIndex = modelList.length ? (currentModelIndex + 1) % modelList.length : 0;
5759
} else {
58-
currentModelIndex = (currentModelIndex - 1 + models.length) % models.length;
60+
currentModelIndex = modelList.length ? (currentModelIndex - 1 + modelList.length) % modelList.length : 0;
5961
}
6062
persistModel();
6163
}
6264
65+
async function loadModels() {
66+
if (!apiKeyInput) return;
67+
modelsLoading = true;
68+
try {
69+
const list = await fetchOpenRouterModels(apiKeyInput, true);
70+
if (Array.isArray(list) && list.length) {
71+
modelList = list;
72+
// Re-align index to saved model if present
73+
const savedModel = guardianHelpers.load()?.model as string | undefined;
74+
if (savedModel) {
75+
const idx = modelList.indexOf(savedModel);
76+
currentModelIndex = idx >= 0 ? idx : 0;
77+
} else {
78+
currentModelIndex = 0;
79+
}
80+
}
81+
} catch (e) {
82+
// Ignore; keep fallback list
83+
} finally {
84+
modelsLoading = false;
85+
}
86+
}
87+
6388
onMount(() => {
6489
// Load saved guardian data
6590
const saved = guardianHelpers.load();
@@ -76,6 +101,8 @@
76101
77102
// Get model selection from guardian storage
78103
loadModelFromStorage();
104+
// Fetch model list from OpenRouter if API key is present
105+
loadModels();
79106
});
80107
81108
function toggleTheme(direction: 'prev' | 'next') {
@@ -233,10 +260,11 @@
233260
<!-- Model selection (OpenRouter) -->
234261
<div class="cli-row px-2 py-1">
235262
<span class="label" style="color: var(--petalytics-foam);">model</span>
236-
<span class="value" style="color: var(--petalytics-text);">{displayModel()}</span>
263+
<span class="value" style="color: var(--petalytics-text);">{displayModel()} {#if modelsLoading}(loading...){/if}</span>
237264
<div class="ml-2 flex items-center space-x-1">
238265
<button type="button" class="arrow-btn" onclick={() => cycleModel('prev')} aria-label="Previous model">&lt;</button>
239266
<button type="button" class="arrow-btn" onclick={() => cycleModel('next')} aria-label="Next model">&gt;</button>
267+
<button type="button" class="arrow-btn" onclick={loadModels} aria-label="Refresh models">↻</button>
240268
</div>
241269
</div>
242270

src/lib/components/panels/Viewport.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
import EmptyState from '../ui/EmptyState.svelte';
99
import Skeleton from '../ui/Skeleton.svelte';
1010
import { rightPanelView, uiHelpers } from '$lib/stores/ui';
11+
import type { RightPanelView } from '$lib/stores/ui';
1112
import type { PetPanelData } from '$lib/types/Pet';
1213
import type { JournalEntry } from '$lib/types/JournalEntry';
1314
1415
let selectedPet: PetPanelData | null = null;
1516
let selectedPetId: string | null = null;
1617
let pets: PetPanelData[] = [];
17-
let currentView: 'dashboard' | 'journal' | 'history' | 'memories' | 'confirmArchive' = 'dashboard';
18+
let currentView: RightPanelView = 'dashboard';
1819
let journalInput = '';
1920
let selectedMood = '';
2021
let selectedActivity = '';

src/lib/stores/ai-analysis.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ let analyzer: AIAnalyzer | null = null;
1212
// Initialize analyzer when API key is available
1313
guardianStore.subscribe((guardian) => {
1414
if (guardian.apiKey && guardian.apiKeyValid) {
15-
analyzer = new AIAnalyzer(guardian.apiKey, (guardian as any).model || 'anthropic/claude-3.5-sonnet');
15+
analyzer = new AIAnalyzer(guardian.apiKey, (guardian as any).model || 'openai/gpt-oss-20b:free');
1616
} else {
1717
analyzer = null;
1818
}

src/lib/stores/guardian.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const STORAGE_KEY = 'petalytics-guardian';
77
const defaultGuardian = {
88
name: '',
99
apiKey: '',
10-
model: 'anthropic/claude-3.5-sonnet',
10+
model: 'openai/gpt-oss-20b:free',
1111
preferences: {
1212
dailyReminders: false,
1313
aiInsights: true,

src/lib/utils/ai-analysis.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class AIAnalyzer {
1515
private baseUrl = 'https://openrouter.ai/api/v1/chat/completions';
1616
private model: string;
1717

18-
constructor(apiKey: string, model: string = 'anthropic/claude-3.5-sonnet') {
18+
constructor(apiKey: string, model: string = 'openai/gpt-oss-20b:free') {
1919
this.apiKey = apiKey;
2020
this.model = model;
2121
}
@@ -143,3 +143,25 @@ Consider breed-specific traits, age-related needs, and behavioral patterns. Keep
143143
}
144144
}
145145
}
146+
147+
// Fetch available models from OpenRouter API
148+
export async function fetchOpenRouterModels(apiKey: string, onlyFree: boolean = true): Promise<string[]> {
149+
const referer = typeof window !== 'undefined' ? window.location.origin : undefined;
150+
const resp = await fetch('https://openrouter.ai/api/v1/models', {
151+
method: 'GET',
152+
headers: {
153+
Authorization: `Bearer ${apiKey}`,
154+
'Content-Type': 'application/json',
155+
...(referer ? { 'HTTP-Referer': referer } : {}),
156+
'X-Title': 'Petalytics',
157+
},
158+
});
159+
if (!resp.ok) {
160+
throw new Error(`Failed to list models: ${resp.status}`);
161+
}
162+
const data = await resp.json();
163+
const ids: string[] = (data?.data || []).map((m: any) => m?.id).filter(Boolean);
164+
const filtered = onlyFree ? ids.filter((id) => /:free$/i.test(id)) : ids;
165+
// Deduplicate and sort for stability
166+
return Array.from(new Set(filtered)).sort((a, b) => a.localeCompare(b));
167+
}

0 commit comments

Comments
 (0)