From 9dd63b50977ac71427a620230050a01f1195e751 Mon Sep 17 00:00:00 2001 From: Minhyun Kim Date: Wed, 16 Sep 2026 02:28:54 +0000 Subject: [PATCH] Accept a canonical Hugging Face model URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ainize chat https://huggingface.co/gpt2` was refused. The parser required `/`, but the oldest and most-used repositories on the Hub are canonical: they sit at the root with no owner, and their ID is the bare name a runtime serves them under. `gpt2`, `distilgpt2`, `bert-base-uncased` were all unreachable this way, and pointing at the rewritten `openai-community/gpt2` does not help — that is a different string, so a node actually serving `gpt2` answers "not served by this node". Both shapes now parse. Dataset, Space and docs paths are still refused, by a reserved-prefix list rather than by segment count, and credentials, queries and fragments are refused as before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019hZqdLKnVfzvRf9mus6NNh --- src/commands/chat.ts | 20 +++++++++++++++++--- test/huggingface-model-chat.test.ts | 10 ++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/commands/chat.ts b/src/commands/chat.ts index a73c7d3..93c66e8 100644 --- a/src/commands/chat.ts +++ b/src/commands/chat.ts @@ -121,13 +121,27 @@ export function parsePatchIds(input: string | string[] | undefined): string[] { const CHAT_TIMEOUT_MS = 15 * 60_000; // apply + two generations on a busy runtime +/** + * The repository ID inside a Hugging Face model URL. + * + * Most repositories are `/`, but the oldest and most-used models are canonical: they + * live at the root of the Hub with no owner segment, and their ID is the bare name a runtime serves + * them under — `gpt2`, `bert-base-uncased`, `distilgpt2`. Requiring two segments refused those, and + * the rewritten `openai-community/gpt2` form is a different string, so a node serving `gpt2` would + * have rejected it as a different model. Both shapes are accepted; a dataset, Space or file URL is + * still refused, and so is anything with credentials, a query or a fragment. + */ export function huggingFaceModelId(input: string): string { let url: URL; try { url = new URL(input); } catch { throw new CliError('Expected a Hugging Face model repository URL'); } - const match = /^\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/?$/.exec(url.pathname); + const segment = '[A-Za-z0-9_.-]+'; + const match = new RegExp(`^/(?:(${segment})/)?(${segment})/?$`).exec(url.pathname); + const reserved = ['datasets', 'spaces', 'models', 'organizations', 'settings', 'docs', 'blog', 'api']; if (url.origin !== 'https://huggingface.co' || url.username || url.password || url.search || url.hash - || !match || ['datasets', 'spaces'].includes(match[1])) throw new CliError('Use https://huggingface.co//, not a dataset, Space or file URL'); - return `${match[1]}/${match[2]}`; + || !match || reserved.includes(match[1] ?? match[2])) { + throw new CliError('Use https://huggingface.co// (or a canonical https://huggingface.co/), not a dataset, Space or file URL'); + } + return match[1] ? `${match[1]}/${match[2]}` : match[2]; } function chatIds(input: string | string[], opts: ChatArgs): string[] { diff --git a/test/huggingface-model-chat.test.ts b/test/huggingface-model-chat.test.ts index 4b6085f..0fd3046 100644 --- a/test/huggingface-model-chat.test.ts +++ b/test/huggingface-model-chat.test.ts @@ -10,6 +10,16 @@ import { huggingFaceModelId } from '../src/commands/chat.js'; test('model URL parsing rejects datasets, Spaces, credentials and ambiguous revisions', () => { assert.equal(huggingFaceModelId('https://huggingface.co/owner/model/'), 'owner/model'); + // Canonical repositories have no owner segment and their ID is the bare name a runtime serves + // them under. The rewritten `openai-community/gpt2` is a different string, so a node serving + // `gpt2` must be reachable by `gpt2`. + assert.equal(huggingFaceModelId('https://huggingface.co/gpt2'), 'gpt2'); + assert.equal(huggingFaceModelId('https://huggingface.co/bert-base-uncased/'), 'bert-base-uncased'); + assert.equal(huggingFaceModelId('https://huggingface.co/openai-community/gpt2'), 'openai-community/gpt2'); + for (const url of ['https://huggingface.co/datasets', 'https://huggingface.co/spaces', 'https://huggingface.co/docs', + 'https://huggingface.co/models', 'https://huggingface.co/settings']) { + assert.throws(() => huggingFaceModelId(url), undefined, url); + } for (const url of ['http://huggingface.co/owner/model', 'https://huggingface.co/datasets/model', 'https://huggingface.co/spaces/model', 'https://example.com/owner/model', 'https://secret@huggingface.co/owner/model', 'https://huggingface.co/owner/model/tree/main', 'https://huggingface.co/owner/model?revision=main', 'https://huggingface.co/owner/model#revision']) {