Skip to content

Commit 8c53d86

Browse files
committed
fix(ModelCache): Re-arm empty-model-response throttle for auth-scoped providers + privacy doc fix
1 parent 4c9a6f9 commit 8c53d86

3 files changed

Lines changed: 49 additions & 9 deletions

File tree

PRIVACY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ go—and, importantly, where they don't.
5656
## **Your Choices & Control**
5757

5858
- You can run models locally to prevent data being sent to third-parties.
59-
- Telemetry collection is enabled by default to help us improve Zoo Code, but
60-
you can opt out at any time through the settings.
59+
- Telemetry collection is off by default and requires your explicit opt-in.
60+
You can change your choice at any time through the settings.
6161
- You can delete Zoo Code to stop all data collection.
6262

6363
## **Security & Updates**

src/api/providers/fetchers/__tests__/modelCache.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ vi.mock("fs", () => ({
4343
vi.mock("../litellm")
4444
vi.mock("../openrouter")
4545
vi.mock("../requesty")
46+
vi.mock("../zoo-gateway")
4647

4748
// Mock ContextProxy with a simple static instance
4849
vi.mock("../../../core/config/ContextProxy", () => ({
@@ -64,10 +65,12 @@ import { getModels, getModelsFromCache } from "../modelCache"
6465
import { getLiteLLMModels } from "../litellm"
6566
import { getOpenRouterModels } from "../openrouter"
6667
import { getRequestyModels } from "../requesty"
68+
import { getZooGatewayModels } from "../zoo-gateway"
6769

6870
const mockGetLiteLLMModels = getLiteLLMModels as Mock<typeof getLiteLLMModels>
6971
const mockGetOpenRouterModels = getOpenRouterModels as Mock<typeof getOpenRouterModels>
7072
const mockGetRequestyModels = getRequestyModels as Mock<typeof getRequestyModels>
73+
const mockGetZooGatewayModels = getZooGatewayModels as Mock<typeof getZooGatewayModels>
7174

7275
const DUMMY_REQUESTY_KEY = "requesty-key-for-testing"
7376

@@ -296,6 +299,39 @@ describe("empty cache protection", () => {
296299
expect(result).toEqual(mockModels)
297300
expect(mockSet).toHaveBeenCalledWith("openrouter", mockModels)
298301
})
302+
303+
it("re-arms the empty-response throttle after a non-empty response from an auth-scoped provider", async () => {
304+
// zoo-gateway is auth-scoped and skips caching entirely, but a non-empty response
305+
// must still clear the empty-response throttle so a later empty response is reported again.
306+
mockGetZooGatewayModels.mockResolvedValueOnce({})
307+
308+
await getModels({ provider: "zoo-gateway", apiKey: "test-key" })
309+
310+
expect(TelemetryService.instance.captureEvent).toHaveBeenCalledTimes(1)
311+
312+
const mockModels = {
313+
"zoo-gateway/model": {
314+
maxTokens: 8192,
315+
contextWindow: 128000,
316+
supportsPromptCache: false,
317+
description: "Zoo Gateway model",
318+
},
319+
}
320+
mockGetZooGatewayModels.mockResolvedValueOnce(mockModels)
321+
322+
await getModels({ provider: "zoo-gateway", apiKey: "test-key" })
323+
324+
// Auth-scoped providers never populate the cache.
325+
expect(mockSet).not.toHaveBeenCalled()
326+
327+
mockGetZooGatewayModels.mockResolvedValueOnce({})
328+
329+
await getModels({ provider: "zoo-gateway", apiKey: "test-key" })
330+
331+
// The throttle should have been re-armed by the non-empty response above, so this
332+
// second empty response is reported again instead of being suppressed.
333+
expect(TelemetryService.instance.captureEvent).toHaveBeenCalledTimes(2)
334+
})
299335
})
300336

301337
describe("refreshModels", () => {

src/api/providers/fetchers/modelCache.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -279,15 +279,19 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
279279

280280
// Only cache non-empty results so a failed API response doesn't get persisted
281281
// as if the provider had no models. Auth-scoped providers skip caching entirely.
282-
if (modelCount > 0 && !shouldSkipCache) {
283-
memoryCache.set(cacheKey, models)
282+
if (modelCount > 0) {
283+
// Clear the empty-response throttle for any non-empty response, including from
284+
// auth-scoped providers that skip caching, so a later empty response is reported again.
285+
reportedEmptyModelResponse.delete(cacheKey)
284286

285-
await writeModels(cacheKey, models).catch((err) =>
286-
console.error(`[MODEL_CACHE] Error writing ${cacheKey} models to file cache:`, err),
287-
)
287+
if (!shouldSkipCache) {
288+
memoryCache.set(cacheKey, models)
288289

289-
reportedEmptyModelResponse.delete(cacheKey)
290-
} else if (modelCount === 0) {
290+
await writeModels(cacheKey, models).catch((err) =>
291+
console.error(`[MODEL_CACHE] Error writing ${cacheKey} models to file cache:`, err),
292+
)
293+
}
294+
} else {
291295
captureModelCacheEmptyResponseOnce(provider, cacheKey, { context: "getModels", hasExistingCache: false })
292296
}
293297

0 commit comments

Comments
 (0)