diff --git a/changelog.d/features/9322-nanogpt-endpoint-surface.md b/changelog.d/features/9322-nanogpt-endpoint-surface.md new file mode 100644 index 0000000000..485b3cf6c6 --- /dev/null +++ b/changelog.d/features/9322-nanogpt-endpoint-surface.md @@ -0,0 +1 @@ +- **feat(providers):** expanded the NanoGPT (`nano-gpt.com`) upstream provider from chat-only to the full OpenAI-compatible endpoint surface: audio transcriptions (`/api/v1/audio/transcriptions`), audio speech (`/api/v1/audio/speech`), video generation (`/api/v1/video/generations`), embeddings (`/v1/embeddings`), and the Responses API (`responsesBaseUrl` → `/api/v1/responses`) ([#9322](https://github.com/diegosouzapw/OmniRoute/issues/9322)) diff --git a/open-sse/config/audioRegistry.ts b/open-sse/config/audioRegistry.ts index 8877ca0257..06d32d93af 100644 --- a/open-sse/config/audioRegistry.ts +++ b/open-sse/config/audioRegistry.ts @@ -247,6 +247,17 @@ export const AUDIO_TRANSCRIPTION_PROVIDERS: Record = { format: "speechmatics", models: [{ id: "enhanced", name: "Enhanced" }], }, + + nanogpt: { + id: "nanogpt", + baseUrl: "https://nano-gpt.com/api/v1/audio/transcriptions", + authType: "apikey", + authHeader: "bearer", + models: [ + { id: "whisper-1", name: "Whisper 1" }, + { id: "gpt-4o-transcription", name: "GPT-4o Transcription" }, + ], + }, }; /** @@ -570,6 +581,17 @@ export const AUDIO_SPEECH_PROVIDERS: Record = { { id: "mimo-v2.5-tts-voiceclone", name: "MiMo V2.5 Voice Clone" }, ], }, + + nanogpt: { + id: "nanogpt", + baseUrl: "https://nano-gpt.com/api/v1/audio/speech", + authType: "apikey", + authHeader: "bearer", + models: [ + { id: "tts-1-hd", name: "TTS 1 HD" }, + { id: "tts-1", name: "TTS 1" }, + ], + }, }; /** diff --git a/open-sse/config/embeddingRegistry.ts b/open-sse/config/embeddingRegistry.ts index f8de0e18e9..c0c84f94fa 100644 --- a/open-sse/config/embeddingRegistry.ts +++ b/open-sse/config/embeddingRegistry.ts @@ -409,6 +409,25 @@ export const EMBEDDING_PROVIDERS: Record = { }, ], }, + + nanogpt: { + id: "nanogpt", + baseUrl: "https://nano-gpt.com/v1/embeddings", + authType: "apikey", + authHeader: "bearer", + models: [ + { + id: "text-embedding-3-small", + name: "Text Embedding 3 Small", + dimensions: 1536, + }, + { + id: "text-embedding-3-large", + name: "Text Embedding 3 Large", + dimensions: 3072, + }, + ], + }, }; const EMBEDDING_PROVIDER_ALIASES: Record = { diff --git a/open-sse/config/providers/registry/nanogpt/index.ts b/open-sse/config/providers/registry/nanogpt/index.ts index 9947938ffb..1c95c8bb52 100644 --- a/open-sse/config/providers/registry/nanogpt/index.ts +++ b/open-sse/config/providers/registry/nanogpt/index.ts @@ -8,6 +8,7 @@ export const nanogptProvider: RegistryEntry = { executor: "default", baseUrl: "https://nano-gpt.com/api/v1/chat/completions", modelsUrl: "https://nano-gpt.com/api/v1/models", + responsesBaseUrl: "https://nano-gpt.com/api/v1/responses", authType: "apikey", authHeader: "bearer", models: CHAT_OPENAI_COMPAT_MODELS.nanogpt, diff --git a/open-sse/config/videoRegistry.ts b/open-sse/config/videoRegistry.ts index ee8dfffa57..6121fffe45 100644 --- a/open-sse/config/videoRegistry.ts +++ b/open-sse/config/videoRegistry.ts @@ -339,6 +339,15 @@ export const VIDEO_PROVIDERS: Record = { format: "adobe-firefly-video", models: toRegistryVideoModels(), }, + + nanogpt: { + id: "nanogpt", + baseUrl: "https://nano-gpt.com/api/v1/video/generations", + authType: "apikey", + authHeader: "bearer", + format: "openai", + models: [{ id: "default", name: "NanoGPT Video" }], + }, }; /** diff --git a/tests/unit/nanogpt-endpoint-surface.test.ts b/tests/unit/nanogpt-endpoint-surface.test.ts new file mode 100644 index 0000000000..a34afab113 --- /dev/null +++ b/tests/unit/nanogpt-endpoint-surface.test.ts @@ -0,0 +1,131 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + AUDIO_TRANSCRIPTION_PROVIDERS, + AUDIO_SPEECH_PROVIDERS, + getTranscriptionProvider, + getSpeechProvider, +} from "../../open-sse/config/audioRegistry.ts"; +import { VIDEO_PROVIDERS, getVideoProvider } from "../../open-sse/config/videoRegistry.ts"; +import { + EMBEDDING_PROVIDERS, + getEmbeddingProvider, +} from "../../open-sse/config/embeddingRegistry.ts"; +import { REGISTRY } from "../../open-sse/config/providerRegistry.ts"; + +describe("nanogpt endpoint surface (#9322)", () => { + describe("audio transcription", () => { + it("is registered in AUDIO_TRANSCRIPTION_PROVIDERS", () => { + assert.ok( + AUDIO_TRANSCRIPTION_PROVIDERS.nanogpt, + "nanogpt should be in AUDIO_TRANSCRIPTION_PROVIDERS" + ); + }); + + it("resolves nanogpt transcription config", () => { + const p = getTranscriptionProvider("nanogpt"); + assert.ok(p, "getTranscriptionProvider should resolve nanogpt"); + assert.equal(p.id, "nanogpt"); + assert.equal(p.baseUrl, "https://nano-gpt.com/api/v1/audio/transcriptions"); + assert.equal(p.authType, "apikey"); + assert.equal(p.authHeader, "bearer"); + }); + + it("has at least one transcription model", () => { + const p = getTranscriptionProvider("nanogpt"); + assert.ok(p.models.length >= 1, "Expected >= 1 transcription model"); + }); + }); + + describe("audio speech", () => { + it("is registered in AUDIO_SPEECH_PROVIDERS", () => { + assert.ok( + AUDIO_SPEECH_PROVIDERS.nanogpt, + "nanogpt should be in AUDIO_SPEECH_PROVIDERS" + ); + }); + + it("resolves nanogpt speech config", () => { + const p = getSpeechProvider("nanogpt"); + assert.ok(p, "getSpeechProvider should resolve nanogpt"); + assert.equal(p.id, "nanogpt"); + assert.equal(p.baseUrl, "https://nano-gpt.com/api/v1/audio/speech"); + assert.equal(p.authType, "apikey"); + assert.equal(p.authHeader, "bearer"); + }); + + it("has at least one speech model", () => { + const p = getSpeechProvider("nanogpt"); + assert.ok(p.models.length >= 1, "Expected >= 1 speech model"); + }); + }); + + describe("video generation", () => { + it("is registered in VIDEO_PROVIDERS", () => { + assert.ok( + VIDEO_PROVIDERS.nanogpt, + "nanogpt should be in VIDEO_PROVIDERS" + ); + }); + + it("resolves nanogpt video config", () => { + const p = getVideoProvider("nanogpt"); + assert.ok(p, "getVideoProvider should resolve nanogpt"); + assert.equal(p.id, "nanogpt"); + assert.equal(p.baseUrl, "https://nano-gpt.com/api/v1/video/generations"); + assert.equal(p.authType, "apikey"); + assert.equal(p.authHeader, "bearer"); + }); + + it("has at least one video model", () => { + const p = getVideoProvider("nanogpt"); + assert.ok(p.models.length >= 1, "Expected >= 1 video model"); + }); + }); + + describe("embeddings", () => { + it("is registered in EMBEDDING_PROVIDERS", () => { + assert.ok( + EMBEDDING_PROVIDERS.nanogpt, + "nanogpt should be in EMBEDDING_PROVIDERS" + ); + }); + + it("resolves nanogpt embedding config", () => { + const p = getEmbeddingProvider("nanogpt"); + assert.ok(p, "getEmbeddingProvider should resolve nanogpt"); + assert.equal(p.id, "nanogpt"); + assert.equal(p.baseUrl, "https://nano-gpt.com/v1/embeddings"); + assert.equal(p.authType, "apikey"); + assert.equal(p.authHeader, "bearer"); + }); + + it("has at least one embedding model", () => { + const p = getEmbeddingProvider("nanogpt"); + assert.ok(p.models.length >= 1, "Expected >= 1 embedding model"); + }); + }); + + describe("registry entry", () => { + it("has a registry entry with the canonical identity", () => { + const entry = REGISTRY.nanogpt; + assert.ok(entry, "REGISTRY.nanogpt should be defined"); + assert.equal(entry.id, "nanogpt"); + assert.equal(entry.format, "openai"); + assert.equal(entry.executor, "default"); + }); + + it("has responsesBaseUrl for Responses API", () => { + const entry = REGISTRY.nanogpt; + assert.ok(entry, "REGISTRY.nanogpt should be defined"); + assert.ok( + entry.responsesBaseUrl, + "nanogpt registry entry should have responsesBaseUrl" + ); + assert.equal( + entry.responsesBaseUrl, + "https://nano-gpt.com/api/v1/responses" + ); + }); + }); +});