fix: portail laissant passer /health mais bloquant /v1 traité comme liaison en échec (v2.4.0.3) (#18)

- Une page web reçue sur /v1/capabilities fait échouer la connexion et le test
  de liaison même si /health répond, ce qui déclenche la recherche de l'API.
- La découverte sonde /v1/capabilities puis /v1/models (JSON exigé, 401/403
  accepté) avant /health, pour ne pas prendre un portail pour l'API.


Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
LogiFlowandClaude authored and GitHub committed 2026-09-04 08:22:19 +02:00
1 parent df25dd591c
commit d1b12c16c5
5 files changed
+26 -10

No files matched your search

@@ -108,6 +108,7 @@ export function SettingsDrawer({ onClose }: Props) {
caps = ` · modèle ${String(c.model ?? '?')} · ${Object.entries(c.features ?? {}).filter(([, v]) => v === true).length} fonctions`;
} catch (err) {
const message = (err as Error).message;
if (/page web/.test(message)) throw err;
caps = /404/.test(message)
? ' · /v1/capabilities absent (Hermes ancien : transport chat completions)'
: ` · capabilities : ${message}`;
+21 -9
View File
@@ -72,18 +72,30 @@ export function hermesUrlCandidates(url: string): string[] {
return [...out];
}
/** Probe candidate URLs until one answers Hermes JSON on /health or /v1/capabilities. */
/** True when this base URL answers like the Hermes API (JSON on /v1/*, or 401/403 = key required). */
export async function probeHermesUrl(config: HermesConfig, url: string): Promise<boolean> {
const client = new HermesClient({ ...config, url });
for (const path of ['/v1/capabilities', '/v1/models', '/health']) {
try {
const payload = await client.request<unknown>(path, { timeoutMs: 4000 });
if (payload && typeof payload === 'object') return true;
} catch (err) {
if (err instanceof HttpError) {
if (err.status === 401 || err.status === 403) return true; // the API is there, only the key is missing
if (/page web/.test(err.message)) return false; // a portal answers on this host: not the API
if (err.status === 404) continue; // older Hermes without this endpoint
}
return false;
}
}
return false;
}
/** Probe candidate URLs until one answers like the Hermes API. */
export async function discoverHermesUrl(config: HermesConfig, onProgress?: (url: string) => void): Promise<string | null> {
for (const candidate of hermesUrlCandidates(config.url)) {
onProgress?.(candidate);
const client = new HermesClient({ ...config, url: candidate });
try {
await client.request<unknown>('/health', { timeoutMs: 4000 });
return candidate;
} catch (err) {
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) return candidate; // API found, key missing
continue;
}
if (await probeHermesUrl(config, candidate)) return candidate;
}
return null;
}
+2
View File
@@ -122,6 +122,8 @@ export const useHermes = create<HermesStore>((set, get) => ({
try {
capabilities = await client.capabilities();
} catch (err) {
// A web page on /v1/* means the whole API is behind a portal even if /health passed through.
if (/page web/.test((err as Error).message)) throw err;
Log.warn('hermes', `capabilities unavailable: ${(err as Error).message}`);
}
const transport = resolveTransport(config, capabilities);