diff --git a/package.json b/package.json index acc1cec..72b5201 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "description": "The official Requesty extension for the Pi Coding Agent", "type": "module", "main": "requesty.js", + "scripts": { + "test": "node --test" + }, "keywords": ["pi-package", "pi", "pi-coding-agent", "pi-extensions", "requesty", "openai-compatible", "models"], "license": "MIT", "peerDependencies": { diff --git a/requesty.js b/requesty.js index 5ab8906..c918f22 100644 --- a/requesty.js +++ b/requesty.js @@ -9,10 +9,15 @@ const DEFAULT_NAME = "Requesty"; const DEFAULT_CONTEXT_WINDOW = 128000; const DEFAULT_MAX_TOKENS = 4096; -function normalizeBaseUrl(baseUrl) { +export function normalizeBaseUrl(baseUrl) { return baseUrl.replace(/\/+$/, ""); } +export function modelsUrl(baseUrl) { + const base = normalizeBaseUrl(baseUrl); + return /\/v\d+$/.test(base) ? `${base}/models` : `${base}/v1/models`; +} + function readModelsJson() { if (!fs.existsSync(MODELS_JSON_PATH)) { throw new Error(`${MODELS_JSON_PATH} does not exist`); @@ -56,7 +61,7 @@ function getRequestyConfig() { } async function discoverModels(provider) { - const response = await fetch(`${provider.baseUrl}/models`, { + const response = await fetch(modelsUrl(provider.baseUrl), { headers: { Authorization: `Bearer ${provider.apiKey}` }, }); diff --git a/requesty.test.js b/requesty.test.js new file mode 100644 index 0000000..e5f41fc --- /dev/null +++ b/requesty.test.js @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { modelsUrl, normalizeBaseUrl } from "./requesty.js"; + +test("normalizeBaseUrl strips trailing slashes", () => { + const cases = [ + ["https://router.requesty.ai", "https://router.requesty.ai"], + ["https://router.requesty.ai/", "https://router.requesty.ai"], + ["https://router.requesty.ai///", "https://router.requesty.ai"], + ["https://router.requesty.ai/v1", "https://router.requesty.ai/v1"], + ["https://router.requesty.ai/v1/", "https://router.requesty.ai/v1"], + ]; + + for (const [input, expected] of cases) { + assert.equal(normalizeBaseUrl(input), expected); + } +}); + +test("modelsUrl always resolves to a single /v1/models endpoint", () => { + const cases = [ + // Anthropic Messages config: baseUrl has no version segment. + ["https://router.requesty.ai", "https://router.requesty.ai/v1/models"], + ["https://router.requesty.ai/", "https://router.requesty.ai/v1/models"], + // OpenAI Completions config: baseUrl already ends with a version segment. + ["https://router.requesty.ai/v1", "https://router.requesty.ai/v1/models"], + ["https://router.requesty.ai/v1/", "https://router.requesty.ai/v1/models"], + // Other version segments are respected rather than doubled up. + ["https://router.requesty.ai/v2", "https://router.requesty.ai/v2/models"], + ]; + + for (const [input, expected] of cases) { + assert.equal(modelsUrl(input), expected); + } +});