Skip to content

Commit ccab3f3

Browse files
committed
feat: enhance model fetching and tool call handling in AI components
- Updated the model fetching logic to include persistent storage for improved performance and reliability. - Introduced background model list updates to ensure the UI reflects the latest data. - Enhanced the handling of parameterless tool calls in the AI provider, injecting default arguments when necessary. - Refactored the chat adapter to support session rollbacks, improving user experience during message regeneration. These changes significantly improve the functionality and responsiveness of the AI components.
1 parent 436c955 commit ccab3f3

File tree

12 files changed

+1004
-113
lines changed

12 files changed

+1004
-113
lines changed

packages/aipex-react/src/components/chatbot/components/input-area.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ChatStatus } from "ai";
22
import { ClockIcon } from "lucide-react";
33
import { useCallback, useEffect, useMemo, useState } from "react";
44
import { useTranslation } from "../../../i18n/context";
5-
import { fetchModelsForSelector } from "../../../lib/models";
5+
import { fetchModelsForSelector, onModelListChange } from "../../../lib/models";
66
import { cn } from "../../../lib/utils";
77
import type { ContextItem, InputAreaProps } from "../../../types";
88
import {
@@ -91,8 +91,19 @@ export function DefaultInputArea({
9191
setIsLoadingModels(false);
9292
}
9393
});
94+
95+
// Subscribe to background model list updates (e.g. server returned newer data)
96+
const unsubscribe = onModelListChange((updatedModels) => {
97+
if (!cancelled) {
98+
setFetchedModels(
99+
updatedModels.map((m) => ({ name: m.name, value: m.id })),
100+
);
101+
}
102+
});
103+
94104
return () => {
95105
cancelled = true;
106+
unsubscribe();
96107
};
97108
}, []);
98109

packages/aipex-react/src/hooks/use-chat.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export function useChat(
285285
return;
286286
}
287287

288-
// Remove last assistant message
288+
// Remove last assistant message from UI
289289
const removed = adapter.removeLastAssistantMessage();
290290
if (!removed) return;
291291

@@ -302,6 +302,9 @@ export function useChat(
302302
const text = textPart?.type === "text" ? textPart.text : "";
303303

304304
if (sessionId && text) {
305+
// Roll back the session so the agent doesn't see the old assistant turn
306+
await agent.rollbackLastAssistantTurn(sessionId);
307+
305308
adapter.setStatus("submitted");
306309
const events = agent.chat(text, { sessionId });
307310
await processAgentEvents(events);

packages/aipex-react/src/lib/models.ts

Lines changed: 134 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ const FALLBACK_MODELS: ModelInfo[] = [
7070
];
7171

7272
const MODELS_API_URL = "https://www.claudechrome.com/api/models";
73+
const STORAGE_KEY = "cachedModelList";
74+
const STORAGE_TIMESTAMP_KEY = "cachedModelListTimestamp";
75+
const MAX_MODELS = 200;
7376

74-
// Convert API pricing to price level
7577
function getPriceLevel(
7678
pricing: ApiModelPricing,
7779
): "cheap" | "normal" | "expensive" {
@@ -81,7 +83,6 @@ function getPriceLevel(
8183
return "expensive";
8284
}
8385

84-
// Convert API model to internal ModelInfo
8586
function convertApiModel(apiModel: ApiModel): ModelInfo {
8687
return {
8788
id: apiModel.id,
@@ -97,15 +98,13 @@ function convertApiModel(apiModel: ApiModel): ModelInfo {
9798
};
9899
}
99100

100-
// Validate that the API response matches the expected schema
101101
function isValidApiResponse(data: unknown): data is ApiResponse {
102102
if (typeof data !== "object" || data === null) return false;
103103
const obj = data as Record<string, unknown>;
104104
if (typeof obj.success !== "boolean") return false;
105105
if (typeof obj.data !== "object" || obj.data === null) return false;
106106
const d = obj.data as Record<string, unknown>;
107107
if (!Array.isArray(d.models)) return false;
108-
// Validate first model shape if present
109108
if (d.models.length > 0) {
110109
const first = d.models[0] as Record<string, unknown>;
111110
if (typeof first.id !== "string" || typeof first.name !== "string") {
@@ -115,53 +114,164 @@ function isValidApiResponse(data: unknown): data is ApiResponse {
115114
return true;
116115
}
117116

118-
// Cache for models
117+
// --- Persistent storage helpers ---
118+
119+
async function loadFromStorage(): Promise<ModelInfo[] | null> {
120+
try {
121+
if (typeof chrome !== "undefined" && chrome.storage?.local) {
122+
const result = await chrome.storage.local.get([STORAGE_KEY]);
123+
const models = result[STORAGE_KEY];
124+
if (Array.isArray(models) && models.length > 0) {
125+
return models as ModelInfo[];
126+
}
127+
}
128+
} catch {
129+
// Storage not available (e.g. in tests)
130+
}
131+
return null;
132+
}
133+
134+
async function saveToStorage(
135+
models: ModelInfo[],
136+
serverTimestamp: number,
137+
): Promise<void> {
138+
try {
139+
if (typeof chrome !== "undefined" && chrome.storage?.local) {
140+
await chrome.storage.local.set({
141+
[STORAGE_KEY]: models,
142+
[STORAGE_TIMESTAMP_KEY]: serverTimestamp,
143+
});
144+
}
145+
} catch {
146+
// Ignore storage errors
147+
}
148+
}
149+
150+
async function getStoredTimestamp(): Promise<number> {
151+
try {
152+
if (typeof chrome !== "undefined" && chrome.storage?.local) {
153+
const result = await chrome.storage.local.get([STORAGE_TIMESTAMP_KEY]);
154+
const ts = result[STORAGE_TIMESTAMP_KEY];
155+
if (typeof ts === "number") return ts;
156+
}
157+
} catch {
158+
// Ignore
159+
}
160+
return 0;
161+
}
162+
163+
// --- In-memory cache (fast path) ---
164+
119165
let cachedModels: ModelInfo[] | null = null;
120-
let lastFetchTime = 0;
121-
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
122-
const MAX_MODELS = 200; // Safety cap on number of models
166+
let cachedServerTimestamp = 0;
167+
let storageLoaded = false;
123168

124169
/**
125-
* Fetch models from the server API with caching and fallback.
126-
* Returns cached result if still valid (5 min TTL).
127-
* Falls back to FALLBACK_MODELS on any error.
170+
* Fetch models with a two-tier cache:
171+
* 1. In-memory cache (instant)
172+
* 2. chrome.storage.local (survives service worker restarts)
173+
*
174+
* On the first call, returns storage-cached models immediately.
175+
* A background fetch updates both caches when the server reports new data.
128176
*/
129177
export async function fetchModels(): Promise<ModelInfo[]> {
130-
// Return cached models if still valid
131-
if (cachedModels && Date.now() - lastFetchTime < CACHE_DURATION) {
178+
// 1. Fast path: in-memory cache
179+
if (cachedModels) {
180+
// Trigger background refresh (fire-and-forget)
181+
void refreshFromServer();
132182
return cachedModels;
133183
}
134184

185+
// 2. Try loading from persistent storage
186+
if (!storageLoaded) {
187+
storageLoaded = true;
188+
const stored = await loadFromStorage();
189+
if (stored) {
190+
cachedModels = stored;
191+
cachedServerTimestamp = await getStoredTimestamp();
192+
// Trigger background refresh
193+
void refreshFromServer();
194+
return cachedModels;
195+
}
196+
}
197+
198+
// 3. Nothing cached: fetch synchronously and return
199+
return await fetchFromServer();
200+
}
201+
202+
let refreshInFlight = false;
203+
204+
async function refreshFromServer(): Promise<void> {
205+
if (refreshInFlight) return;
206+
refreshInFlight = true;
135207
try {
136-
const response = await fetch(MODELS_API_URL);
137-
console.log("response", response);
208+
await fetchFromServer();
209+
} finally {
210+
refreshInFlight = false;
211+
}
212+
}
138213

214+
async function fetchFromServer(): Promise<ModelInfo[]> {
215+
try {
216+
const response = await fetch(MODELS_API_URL);
139217
if (!response.ok) {
140218
throw new Error(`API request failed: ${response.status}`);
141219
}
142220

143221
const data: unknown = await response.json();
144-
console.log("data", data);
145222

146223
if (!isValidApiResponse(data)) {
147224
throw new Error("Invalid API response structure");
148225
}
149226

150227
if (data.success && data.data.models.length > 0) {
151-
// Apply safety cap
152-
const models = data.data.models.slice(0, MAX_MODELS).map(convertApiModel);
153-
cachedModels = models;
154-
lastFetchTime = Date.now();
155-
return cachedModels;
228+
const serverTimestamp = data.data.cache?.lastUpdate ?? Date.now();
229+
230+
// Only update if the server data is newer
231+
if (serverTimestamp > cachedServerTimestamp) {
232+
const models = data.data.models
233+
.slice(0, MAX_MODELS)
234+
.map(convertApiModel);
235+
cachedModels = models;
236+
cachedServerTimestamp = serverTimestamp;
237+
await saveToStorage(models, serverTimestamp);
238+
// Notify listeners that models changed
239+
notifyModelChange(models);
240+
}
241+
242+
return cachedModels ?? FALLBACK_MODELS;
156243
}
157244

158245
throw new Error("Empty model list from API");
159-
} catch (_error) {
160-
// Return fallback - do not log sensitive details
161-
return FALLBACK_MODELS;
246+
} catch {
247+
return cachedModels ?? FALLBACK_MODELS;
162248
}
163249
}
164250

251+
// --- Change notification for components ---
252+
253+
type ModelChangeListener = (models: ModelInfo[]) => void;
254+
const modelChangeListeners = new Set<ModelChangeListener>();
255+
256+
function notifyModelChange(models: ModelInfo[]): void {
257+
for (const listener of modelChangeListeners) {
258+
try {
259+
listener(models);
260+
} catch {
261+
// Don't let listener errors break the loop
262+
}
263+
}
264+
}
265+
266+
/**
267+
* Subscribe to model list updates (triggered when server returns new data).
268+
* Returns an unsubscribe function.
269+
*/
270+
export function onModelListChange(listener: ModelChangeListener): () => void {
271+
modelChangeListeners.add(listener);
272+
return () => modelChangeListeners.delete(listener);
273+
}
274+
165275
/**
166276
* Fetch models and convert to the {name, value} format used by the model selector.
167277
*/

packages/browser-ext/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"dependencies": {
3131
"@ai-sdk/anthropic": "^3.0.44",
3232
"@ai-sdk/google": "^3.0.22",
33-
"@ai-sdk/openai": "^3.0.25",
33+
"@ai-sdk/openai": "^3.0.41",
3434
"@ai-sdk/openai-compatible": "^2.0.18",
3535
"@aipexstudio/aipex-core": "workspace:*",
3636
"@aipexstudio/aipex-react": "workspace:*",

0 commit comments

Comments
 (0)