diff --git a/changelog.d/maintenance/release-v3850-basereds-error-helper-20260819.md b/changelog.d/maintenance/release-v3850-basereds-error-helper-20260819.md new file mode 100644 index 0000000000..e89d53ab49 --- /dev/null +++ b/changelog.d/maintenance/release-v3850-basereds-error-helper-20260819.md @@ -0,0 +1,3 @@ +- **fix(ci):** route `open-sse/handlers/imageGeneration/providers/geminiWeb.ts`'s b64_json + download-failure message through `sanitizeErrorMessage()` instead of embedding a raw + `err.message`, clearing the `check:error-helper` base-red on `release/v3.8.50` (#9985). diff --git a/changelog.d/maintenance/release-v3850-basereds-testdrift-20260819.md b/changelog.d/maintenance/release-v3850-basereds-testdrift-20260819.md new file mode 100644 index 0000000000..f4deae6aae --- /dev/null +++ b/changelog.d/maintenance/release-v3850-basereds-testdrift-20260819.md @@ -0,0 +1,12 @@ +- **fix(tests):** drain several base-reds on `release/v3.8.50` (#9985) that were all instances + of the same pattern — a legitimate product change landed without updating the test that + asserted the old behavior: `tests/unit/glm-provider-model-import-route.test.ts` (12 tests) + and `tests/unit/model-sync-route.test.ts` (2 tests) predate #10603's "upstream model sync is + opt-in and manual overrides are preserved" change; `tests/unit/antigravity-model-aliases.test.ts` + predated #10537 retiring the collapsed `gemini-3.7-flash` alias in favor of its three tiered + ids. Also fixes a real data drift in `open-sse/config/freeModelCatalog.data.ts` (the `qwen-web` + free-catalog entry still pointed at the retired `qwen3.8-max-preview` id instead of the + current `qwen3.8-max`), corrects the zh-TW `providers.autoFetchModelsTooltip` string to the + glossary-canonical 快取 instead of 緩存, and removes an unused default export from + `src/lib/oauth/providers/zed-hosted.ts` (the named export already covers every consumer) to + shave one symbol off the `check:dead-code` ratchet regression. diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index decef67c50..3d1777ed65 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -361,7 +361,7 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "qoder", modelId: "deepseek-v4-pro", displayName: "DeepSeek-V4-Pro", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, { provider: "qoder", modelId: "deepseek-v4-flash", displayName: "DeepSeek-V4-Flash", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, { provider: "qoder", modelId: "minimax-m3", displayName: "MiniMax-M3", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qwen-web", modelId: "qwen3.8-max-preview", displayName: "Qwen3.8 Max Preview", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, + { provider: "qwen-web", modelId: "qwen3.8-max", displayName: "Qwen3.8 Max", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, { provider: "qwen-web", modelId: "qwen3.7-max", displayName: "Qwen3.7 Max", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, { provider: "qwen-web", modelId: "qwen3.7-plus", displayName: "Qwen3.7 Plus", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, { provider: "qwen-web", modelId: "qwen3.6-plus", displayName: "Qwen3.6 Plus", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, diff --git a/open-sse/handlers/imageGeneration/providers/geminiWeb.ts b/open-sse/handlers/imageGeneration/providers/geminiWeb.ts index 43c91de8fc..8f131fe83d 100644 --- a/open-sse/handlers/imageGeneration/providers/geminiWeb.ts +++ b/open-sse/handlers/imageGeneration/providers/geminiWeb.ts @@ -20,6 +20,7 @@ import { GeminiWebExecutor } from "../../../executors/gemini-web.ts"; import { fetchRemoteImage } from "@/shared/network/remoteImageFetch"; import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts"; +import { sanitizeErrorMessage } from "../../../utils/error.ts"; /** Each image is one gemini.google.com turn (~30-60s). Cap like chatgpt-web. */ const GEMINI_WEB_IMAGE_N_MAX = 4; @@ -210,7 +211,7 @@ export async function handleGeminiWebImageGeneration({ model, status: 502, startTime, - error: `Gemini Web generated an image but OmniRoute could not download it for b64_json conversion: ${err instanceof Error ? err.message : String(err)}`, + error: `Gemini Web generated an image but OmniRoute could not download it for b64_json conversion: ${sanitizeErrorMessage(err instanceof Error ? err.message : String(err))}`, requestBody, }); } diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 627b7b4383..a7ac344ae8 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -6350,7 +6350,7 @@ "autoFetchModels": "自動獲取上游模型", "autoFetchModelsDisabled": "上游模型自動獲取已禁用", "autoFetchModelsEnabled": "上游模型自動獲取已啟用", - "autoFetchModelsTooltip": "在需要時獲取並緩存上游模型", + "autoFetchModelsTooltip": "在需要時獲取並快取上游模型", "autoFetchModelsToggleFailed": "無法切換上游模型自動獲取", "autoFetchModelsPartialFailure": "某些連接已更新,但上游模型自動獲取並未在所有地方更改", "overridesUpstreamModel": "覆蓋上游", diff --git a/src/lib/oauth/providers/zed-hosted.ts b/src/lib/oauth/providers/zed-hosted.ts index 46a4c1b320..b37c157163 100644 --- a/src/lib/oauth/providers/zed-hosted.ts +++ b/src/lib/oauth/providers/zed-hosted.ts @@ -126,5 +126,3 @@ export const zedHosted = { }, }), }; - -export default zedHosted; diff --git a/tests/unit/antigravity-model-aliases.test.ts b/tests/unit/antigravity-model-aliases.test.ts index c821766058..542573b3a4 100644 --- a/tests/unit/antigravity-model-aliases.test.ts +++ b/tests/unit/antigravity-model-aliases.test.ts @@ -17,8 +17,11 @@ function getPublicModel(id: string) { return ANTIGRAVITY_PUBLIC_MODELS.find((model) => model.id === id) as any; } +// #10537 retired the single-alias `gemini-3.7-flash` (which mapped to the upstream +// `gemini-3.7-flash-tiered`) in favor of three directly-callable tiered public models — +// the suffixed ids now work upstream without the collapsing alias. Keep this list in sync +// with ANTIGRAVITY_PUBLIC_MODELS/ANTIGRAVITY_MODEL_ALIASES instead of the retired bare id. const EXPECTED_FLASH_TIERS = [ - ["gemini-3.7-flash", "Gemini 3.7 Flash"], ["gemini-3.7-flash-high", "Gemini 3.7 Flash (High)"], ["gemini-3.7-flash-medium", "Gemini 3.7 Flash (Medium)"], ["gemini-3.7-flash-low", "Gemini 3.7 Flash (Low)"], diff --git a/tests/unit/glm-provider-model-import-route.test.ts b/tests/unit/glm-provider-model-import-route.test.ts index dd174c93c6..b86c115e70 100644 --- a/tests/unit/glm-provider-model-import-route.test.ts +++ b/tests/unit/glm-provider-model-import-route.test.ts @@ -4,6 +4,12 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +// #10603 made upstream model sync opt-in (isAutoFetchModelsEnabled() now requires +// providerSpecificData.autoFetchModels === true) so remote discovery doesn't overwrite +// manual catalog overrides by default. Every connection below sets it explicitly so the +// mocked `fetch` in each test actually gets called — without it, the route short-circuits +// to the local/cached catalog before ever reaching the network call these tests assert on. + const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-glm-models-")); process.env.DATA_DIR = TEST_DATA_DIR; @@ -29,7 +35,7 @@ test("GLM import uses international coding endpoint when apiRegion is internatio authType: "apikey", name: "glm-intl", apiKey: "glm-key", - providerSpecificData: { apiRegion: "international" }, + providerSpecificData: { apiRegion: "international", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -88,7 +94,7 @@ test("GLM import normalizes custom coding models URLs without duplicating endpoi authType: "apikey", name: `glm-custom-${index}`, apiKey: testCase.apiKey, - providerSpecificData: { baseUrl: testCase.baseUrl }, + providerSpecificData: { baseUrl: testCase.baseUrl, autoFetchModels: true }, }) ); } @@ -129,7 +135,7 @@ test("GLM import falls back to Anthropic model discovery when coding discovery f authType: "apikey", name: "glm-discovery-fallback", apiKey: "glm-key", - providerSpecificData: { apiRegion: "international" }, + providerSpecificData: { apiRegion: "international", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -178,7 +184,7 @@ test("GLM import preserves auth failures instead of falling back across transpor authType: "apikey", name: "glm-auth-fail", apiKey: "bad-key", - providerSpecificData: { apiRegion: "international" }, + providerSpecificData: { apiRegion: "international", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -207,7 +213,7 @@ test("GLMT import shares the GLM coding models endpoint and surfaces provider me authType: "apikey", name: "glmt-intl", apiKey: "glmt-key", - providerSpecificData: { apiRegion: "international" }, + providerSpecificData: { apiRegion: "international", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -241,7 +247,7 @@ test("GLM import uses China coding endpoint when apiRegion is china", async () = authType: "apikey", name: "glm-cn", apiKey: "glm-cn-key", - providerSpecificData: { apiRegion: "china" }, + providerSpecificData: { apiRegion: "china", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -272,7 +278,7 @@ test("GLM China provider import uses the specialized GLM discovery path", async authType: "apikey", name: "glm-cn-provider", apiKey: "glm-cn-key", - providerSpecificData: {}, + providerSpecificData: { autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -304,7 +310,7 @@ test("GLM import defaults to international endpoint when apiRegion is missing", authType: "apikey", name: "glm-default", apiKey: "glm-key", - providerSpecificData: {}, + providerSpecificData: { autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -331,7 +337,7 @@ test("GLM import defaults to international endpoint when apiRegion is invalid", authType: "apikey", name: "glm-bogus", apiKey: "glm-key", - providerSpecificData: { apiRegion: "bogus" }, + providerSpecificData: { apiRegion: "bogus", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -359,7 +365,7 @@ test("GLM import prefers apiKey over accessToken and sends only Authorization Be name: "glm-both-tokens", apiKey: "glm-api-key", accessToken: "glm-access-token", - providerSpecificData: { apiRegion: "international" }, + providerSpecificData: { apiRegion: "international", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -387,7 +393,7 @@ test("GLM import falls back to accessToken when apiKey is absent", async () => { authType: "apikey", name: "glm-access-only", accessToken: "glm-access-token", - providerSpecificData: { apiRegion: "international" }, + providerSpecificData: { apiRegion: "international", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; @@ -414,7 +420,7 @@ test("GLM import falls back to the local catalog on upstream non-OK status codes authType: "apikey", name: "glm-error", apiKey: "glm-key", - providerSpecificData: { apiRegion: "international" }, + providerSpecificData: { apiRegion: "international", autoFetchModels: true }, }); const originalFetch = globalThis.fetch; diff --git a/tests/unit/model-sync-route.test.ts b/tests/unit/model-sync-route.test.ts index 0e71b96c17..ece60f730d 100644 --- a/tests/unit/model-sync-route.test.ts +++ b/tests/unit/model-sync-route.test.ts @@ -471,6 +471,10 @@ test("model sync route writes synced available models for non-Gemini providers t ]); }); +// #10603 ("make upstream model sync opt-in and preserve manual overrides") also applies to +// import/merge mode: a manual custom-model row sharing an id with a discovered model is kept +// as the user-owned overlay instead of being demoted/removed. See the comment above the +// "reports synced managed models separately from preserved manual models" test. test("model sync route import mode merges discovered models without deleting manual models", async () => { await resetStorage(); @@ -512,10 +516,13 @@ test("model sync route import mode merges discovered models without deleting man assert.equal(body.updatedCount, 0); assert.equal(body.syncedAliases, 1); assert.deepEqual(body.modelChanges, { added: 1, removed: 0, updated: 0, total: 1 }); - assert.deepEqual(body.customModelChanges, { added: 0, removed: 1, updated: 0, total: 1 }); + assert.deepEqual(body.customModelChanges, { added: 0, removed: 0, updated: 0, total: 0 }); assert.deepEqual( body.models.map((model) => ({ id: model.id, source: model.source })), - [{ id: "manual-only", source: "manual" }] + [ + { id: "manual-only", source: "manual" }, + { id: "router-v4", source: "manual" }, + ] ); assert.deepEqual( body.importedModels.map((model) => ({ id: model.id, source: model.source })), @@ -820,6 +827,13 @@ test("model sync route forwards cookies, filters built-ins, and syncs aliases fo assert.equal(logs[0].account, "External Sync"); }); +// #10603 ("make upstream model sync opt-in and preserve manual overrides") changed +// importManagedModels() so a manually configured custom-model row that shares an id with +// a synced upstream model is no longer demoted/removed — it stays as the user-owned +// metadata overlay and is merged over the synced base at read time (see the comment in +// src/lib/providerModels/managedModelImport.ts). Only rows already tagged as an +// imported/auto-sync source get pruned. This test predates that change; its expectations +// below reflect the current preserve-manual-overrides behavior. test("model sync route reports synced managed models separately from preserved manual models", async () => { await resetStorage(); @@ -858,10 +872,13 @@ test("model sync route reports synced managed models separately from preserved m assert.equal(body.availableModelsCount, 2); assert.equal(body.importedCount, 1); assert.equal(body.updatedCount, 0); - assert.deepEqual(body.customModelChanges, { added: 0, removed: 1, updated: 0, total: 1 }); + assert.deepEqual(body.customModelChanges, { added: 0, removed: 0, updated: 0, total: 0 }); assert.deepEqual( body.models.map((model) => ({ id: model.id, source: model.source })), - [{ id: "manual-only", source: "manual" }] + [ + { id: "manual-only", source: "manual" }, + { id: "router-v4", source: "manual" }, + ] ); assert.deepEqual( (await modelsDb.getSyncedAvailableModels("openrouter")).map((model) => ({