From 471df45bcb652d8776ae40e90ddfb8acf78934d4 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Thu, 28 May 2026 19:21:08 +0700 Subject: [PATCH] refactor(gitlawb): extract shared opengateway validator factory, fix docs path in test - Extract gitlawb/gitlawb-gmi validators into buildOpengatewayValidator factory - Fix dockerignore-docs-coverage test: update stale docs/AUTO-COMBO.md -> docs/routing/AUTO-COMBO.md --- src/lib/providers/validation.ts | 110 +++++++++--------- tests/unit/dockerignore-docs-coverage.test.ts | 4 +- 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 8e5eafe5de..db1e16bc05 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -3492,6 +3492,55 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi } } + /** + * Build Opengateway-style validators (xiaomi-mimo compatible). + * These providers share a POST /chat/completions auth check pattern and differ + * only in default baseUrl and test model name. + */ + function buildOpengatewayValidator( + defaultBaseUrl: string, + model: string + ) { + return async ({ apiKey, providerSpecificData }: any) => { + try { + const baseUrl = normalizeBaseUrl( + providerSpecificData?.baseUrl || defaultBaseUrl + ); + const chatUrl = `${baseUrl.replace(/\/chat\/completions$/, "")}/chat/completions`; + const res = await validationWrite( + chatUrl, + { + method: "POST", + headers: buildBearerHeaders(apiKey, providerSpecificData), + body: JSON.stringify({ + model, + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }), + }, + isLocal + ); + if (res.status === 401 || res.status === 403) { + return { valid: false, error: "Invalid API key" }; + } + // Any non-auth response (200, 400, 422, 429) means auth passed + return { valid: true, error: null }; + } catch (error: any) { + return toValidationErrorResult(error); + } + }; + } + + // Same as buildOpengatewayValidator but returns an object spreadable into SPECIALTY_VALIDATORS. + // isLocal is captured via closure from the outer function scope. + function buildGitlawbValidators( + configs: [string, string, string][] + ): Record> { + return Object.fromEntries( + configs.map(([id, baseUrl, model]) => [id, buildOpengatewayValidator(baseUrl, model)]) + ); + } + // ── Specialty provider validation ── const SPECIALTY_VALIDATORS = { jules: validateJulesProvider, @@ -3745,62 +3794,11 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi }, // Gitlawb Opengateway — Xiaomi MiMo compatible, same /models endpoint limitation. // Bypass /models probe in favor of chat/completions, matching xiaomi-mimo's pattern. - gitlawb: async ({ apiKey, providerSpecificData }: any) => { - try { - const baseUrl = normalizeBaseUrl( - providerSpecificData?.baseUrl || "https://opengateway.gitlawb.com/v1/xiaomi-mimo" - ); - const chatUrl = `${baseUrl.replace(/\/chat\/completions$/, "")}/chat/completions`; - const res = await validationWrite( - chatUrl, - { - method: "POST", - headers: buildBearerHeaders(apiKey, providerSpecificData), - body: JSON.stringify({ - model: "mimo-v2.5-pro", - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }), - }, - isLocal - ); - if (res.status === 401 || res.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - // Any non-auth response (200, 400, 422, 429) means auth passed - return { valid: true, error: null }; - } catch (error: any) { - return toValidationErrorResult(error); - } - }, - "gitlawb-gmi": async ({ apiKey, providerSpecificData }: any) => { - try { - const baseUrl = normalizeBaseUrl( - providerSpecificData?.baseUrl || "https://opengateway.gitlawb.com/v1/gmi-cloud" - ); - const chatUrl = `${baseUrl.replace(/\/chat\/completions$/, "")}/chat/completions`; - const res = await validationWrite( - chatUrl, - { - method: "POST", - headers: buildBearerHeaders(apiKey, providerSpecificData), - body: JSON.stringify({ - model: "XiaomiMiMo/MiMo-V2.5-Pro", - messages: [{ role: "user", content: "test" }], - max_tokens: 1, - }), - }, - isLocal - ); - if (res.status === 401 || res.status === 403) { - return { valid: false, error: "Invalid API key" }; - } - // Any non-auth response (200, 400, 422, 429) means auth passed - return { valid: true, error: null }; - } catch (error: any) { - return toValidationErrorResult(error); - } - }, + // Uses a factory to share validation logic across Opengateway provider variants. + ...buildGitlawbValidators([ + ["gitlawb", "https://opengateway.gitlawb.com/v1/xiaomi-mimo", "mimo-v2.5-pro"], + ["gitlawb-gmi", "https://opengateway.gitlawb.com/v1/gmi-cloud", "XiaomiMiMo/MiMo-V2.5-Pro"], + ]), // Search providers — use factored validator ...Object.fromEntries( Object.entries(SEARCH_VALIDATOR_CONFIGS).map(([id, configFn]) => [ diff --git a/tests/unit/dockerignore-docs-coverage.test.ts b/tests/unit/dockerignore-docs-coverage.test.ts index 2f8f8c6b36..c3fb6176ea 100644 --- a/tests/unit/dockerignore-docs-coverage.test.ts +++ b/tests/unit/dockerignore-docs-coverage.test.ts @@ -25,7 +25,7 @@ const DOCKERIGNORE = path.resolve(REPO_ROOT, ".dockerignore"); const REQUIRED_DOCS = [ "docs/README.md", "docs/PROVIDERS.md", - "docs/AUTO-COMBO.md", + "docs/routing/AUTO-COMBO.md", "docs/guides/SETUP_GUIDE.md", "docs/guides/TROUBLESHOOTING.md", "docs/reference/API_REFERENCE.md", @@ -154,7 +154,7 @@ test("#2348 .dockerignore keeps every doc the in-product viewer needs", () => { test("#2348 .dockerignore still excludes the heavy i18n tree", () => { const parsed = parseDockerignore(fs.readFileSync(DOCKERIGNORE, "utf8")); - const heavy = "docs/i18n/pt-BR/docs/AUTO-COMBO.md"; + const heavy = "docs/i18n/pt-BR/docs/routing/AUTO-COMBO.md"; assert.ok( isIgnored(heavy, parsed), `${heavy} should be excluded from Docker context but is not — image size will balloon`