fix(nanogpt): validate login key via models endpoint

This commit is contained in:
Jordan Humberto de Souza
2026-02-19 11:51:27 -03:00
parent 751de7b192
commit 65f34a80e8
4 changed files with 95 additions and 6 deletions
+1 -1
View File
@@ -516,7 +516,7 @@ Use `/login` with supported providers:
For `ollama`, API key is optional. Leave it unset for local no-auth instances, or set `OLLAMA_API_KEY` for authenticated hosts.
For `vllm`, paste your key in `/login` (or use `VLLM_API_KEY`). For local no-auth servers, any placeholder value works (for example `vllm-local`).
For `nanogpt`, `/login nanogpt` opens `https://nano-gpt.com/api` and prompts for your `sk-...` key (or set `NANO_GPT_API_KEY`).
For `nanogpt`, `/login nanogpt` opens `https://nano-gpt.com/api` and prompts for your `sk-...` key (or set `NANO_GPT_API_KEY`). Login validates the key via NanoGPT's models endpoint (not a fixed model entitlement).
For `cloudflare-ai-gateway`, set provider base URL to
`https://gateway.ai.cloudflare.com/v1/<account_id>/<gateway_id>/anthropic`
(for example in `~/.omp/agent/models.yml`).
@@ -6,6 +6,13 @@ type OpenAICompatibleValidationOptions = {
signal?: AbortSignal;
};
type ModelListValidationOptions = {
provider: string;
apiKey: string;
modelsUrl: string;
signal?: AbortSignal;
};
const VALIDATION_TIMEOUT_MS = 15_000;
/**
@@ -48,3 +55,38 @@ export async function validateOpenAICompatibleApiKey(options: OpenAICompatibleVa
: `${options.provider} API key validation failed (${response.status})`;
throw new Error(message);
}
/**
* Validate an API key against a provider models endpoint.
*
* Useful for providers where access to specific models may vary by plan and
* should not block key validation.
*/
export async function validateApiKeyAgainstModelsEndpoint(options: ModelListValidationOptions): Promise<void> {
const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
const response = await fetch(options.modelsUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${options.apiKey}`,
},
signal,
});
if (response.ok) {
return;
}
let details = "";
try {
details = (await response.text()).trim();
} catch {
// ignore body parse errors, status is enough
}
const message = details
? `${options.provider} API key validation failed (${response.status}): ${details}`
: `${options.provider} API key validation failed (${response.status})`;
throw new Error(message);
}
+4 -5
View File
@@ -8,12 +8,12 @@
* 3. Paste key into CLI
*/
import { validateOpenAICompatibleApiKey } from "./api-key-validation";
import { validateApiKeyAgainstModelsEndpoint } from "./api-key-validation";
import type { OAuthController } from "./types";
const AUTH_URL = "https://nano-gpt.com/api";
const API_BASE_URL = "https://nano-gpt.com/api/v1";
const VALIDATION_MODEL = "openai/gpt-4o-mini";
const MODELS_URL = `${API_BASE_URL}/models`;
export async function loginNanoGPT(options: OAuthController): Promise<string> {
if (!options.onPrompt) {
@@ -40,11 +40,10 @@ export async function loginNanoGPT(options: OAuthController): Promise<string> {
}
options.onProgress?.("Validating API key...");
await validateOpenAICompatibleApiKey({
await validateApiKeyAgainstModelsEndpoint({
provider: "NanoGPT",
apiKey: trimmed,
baseUrl: API_BASE_URL,
model: VALIDATION_MODEL,
modelsUrl: MODELS_URL,
signal: options.signal,
});
+48
View File
@@ -0,0 +1,48 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import { loginNanoGPT } from "../src/utils/oauth/nanogpt";
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
});
describe("nanogpt login", () => {
it("validates API key without requiring a specific model entitlement", async () => {
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
expect(url).toBe("https://nano-gpt.com/api/v1/models");
expect(init?.method).toBe("GET");
expect(init?.headers).toEqual({ Authorization: "Bearer sk-nano-test" });
return new Response(JSON.stringify({ object: "list", data: [] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
global.fetch = fetchMock as unknown as typeof fetch;
const apiKey = await loginNanoGPT({
onPrompt: async () => "sk-nano-test",
});
expect(apiKey).toBe("sk-nano-test");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("surfaces validation errors from models endpoint", async () => {
const fetchMock = vi.fn(async () => {
return new Response('{"code":"invalid_api_key"}', {
status: 401,
headers: { "Content-Type": "application/json" },
});
});
global.fetch = fetchMock as unknown as typeof fetch;
await expect(
loginNanoGPT({
onPrompt: async () => "sk-nano-test",
}),
).rejects.toThrow("NanoGPT API key validation failed (401)");
});
});