Add Novita provider
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { createApiKeyLogin } from "./api-key-login";
|
||||
import type { ProviderDefinition } from "./types";
|
||||
|
||||
export const loginNovita = createApiKeyLogin({
|
||||
providerLabel: "Novita",
|
||||
authUrl: "https://novita.ai/settings/key-management",
|
||||
instructions: "Create or copy your API key from the Novita dashboard",
|
||||
promptMessage: "Paste your Novita API key",
|
||||
placeholder: "sk_...",
|
||||
validation: {
|
||||
kind: "models-endpoint",
|
||||
provider: "novita",
|
||||
modelsUrl: "https://api.novita.ai/openai/v1/models",
|
||||
},
|
||||
});
|
||||
|
||||
export const novitaProvider = {
|
||||
id: "novita",
|
||||
name: "Novita",
|
||||
login: (cb: Parameters<typeof loginNovita>[0]) => loginNovita(cb),
|
||||
} as const satisfies ProviderDefinition;
|
||||
@@ -34,6 +34,7 @@ import { minimaxCodeCnProvider } from "./minimax-code-cn";
|
||||
import { mistralProvider } from "./mistral";
|
||||
import { moonshotProvider } from "./moonshot";
|
||||
import { nanogptProvider } from "./nanogpt";
|
||||
import { novitaProvider } from "./novita";
|
||||
import { nvidiaProvider } from "./nvidia";
|
||||
import { ollamaProvider } from "./ollama";
|
||||
import { ollamaCloudProvider } from "./ollama-cloud";
|
||||
@@ -110,6 +111,7 @@ const ALL = [
|
||||
fireworksProvider,
|
||||
togetherProvider,
|
||||
nvidiaProvider,
|
||||
novitaProvider,
|
||||
huggingfaceProvider,
|
||||
perplexityProvider,
|
||||
qianfanProvider,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ import {
|
||||
mistralModelManagerOptions,
|
||||
moonshotModelManagerOptions,
|
||||
nanoGptModelManagerOptions,
|
||||
novitaModelManagerOptions,
|
||||
nvidiaModelManagerOptions,
|
||||
ollamaModelManagerOptions,
|
||||
openaiModelManagerOptions,
|
||||
@@ -271,6 +272,13 @@ export const CATALOG_PROVIDERS = [
|
||||
createModelManagerOptions: (config: ModelManagerConfig) => nvidiaModelManagerOptions(config),
|
||||
catalogDiscovery: { label: "NVIDIA" },
|
||||
},
|
||||
{
|
||||
id: "novita",
|
||||
defaultModel: "moonshotai/kimi-k2.7-code",
|
||||
envVars: ["NOVITA_API_KEY"],
|
||||
createModelManagerOptions: (config: ModelManagerConfig) => novitaModelManagerOptions(config),
|
||||
catalogDiscovery: { label: "Novita", allowUnauthenticated: true },
|
||||
},
|
||||
{
|
||||
id: "ollama",
|
||||
defaultModel: "gpt-oss:20b",
|
||||
|
||||
@@ -971,6 +971,100 @@ export function nvidiaModelManagerOptions(
|
||||
return createSimpleOpenAICompletionsOptions("nvidia", "https://integrate.api.nvidia.com/v1", config);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5.5 Novita
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface NovitaModelManagerConfig {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
fetch?: FetchImpl;
|
||||
}
|
||||
|
||||
function novitaArrayIncludes(value: unknown, expected: string): boolean {
|
||||
return Array.isArray(value) && value.some(item => item === expected);
|
||||
}
|
||||
|
||||
function isPublicNovitaModelId(id: string): boolean {
|
||||
return !id.toLowerCase().startsWith("ai_infer_test");
|
||||
}
|
||||
|
||||
function toNovitaCostPerMillion(value: unknown): number {
|
||||
return toPositiveNumber(value, 0) / 1000;
|
||||
}
|
||||
|
||||
function getNovitaCacheReadPricePerMillion(entry: OpenAICompatibleModelRecord): number {
|
||||
const pricing = entry.pricing;
|
||||
if (!isRecord(pricing)) {
|
||||
return 0;
|
||||
}
|
||||
const cacheRead = pricing.input_cache_read;
|
||||
if (!isRecord(cacheRead)) {
|
||||
return 0;
|
||||
}
|
||||
return toNovitaCostPerMillion(cacheRead.price_per_m);
|
||||
}
|
||||
|
||||
function mapNovitaModel(
|
||||
entry: OpenAICompatibleModelRecord,
|
||||
defaults: ModelSpec<"openai-completions">,
|
||||
reference: ModelSpec<"openai-completions"> | undefined,
|
||||
): ModelSpec<"openai-completions"> {
|
||||
const model = mapWithBundledReference(
|
||||
{
|
||||
...entry,
|
||||
name: entry.display_name ?? entry.title ?? entry.name,
|
||||
},
|
||||
defaults,
|
||||
reference,
|
||||
);
|
||||
return {
|
||||
...model,
|
||||
reasoning: novitaArrayIncludes(entry.features, "reasoning"),
|
||||
supportsTools: novitaArrayIncludes(entry.features, "function-calling"),
|
||||
input: toInputCapabilities(entry.input_modalities),
|
||||
cost: {
|
||||
input: toNovitaCostPerMillion(entry.input_token_price_per_m),
|
||||
output: toNovitaCostPerMillion(entry.output_token_price_per_m),
|
||||
cacheRead: getNovitaCacheReadPricePerMillion(entry),
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: toPositiveNumber(entry.context_size, model.contextWindow),
|
||||
maxTokens: toPositiveNumber(entry.max_output_tokens, model.maxTokens),
|
||||
};
|
||||
}
|
||||
|
||||
export function novitaModelManagerOptions(
|
||||
config?: NovitaModelManagerConfig,
|
||||
): ModelManagerOptions<"openai-completions"> {
|
||||
const apiKey = config?.apiKey;
|
||||
const baseUrl = config?.baseUrl ?? "https://api.novita.ai/openai/v1";
|
||||
const references = createBundledReferenceMap<"openai-completions">(
|
||||
"novita" as Parameters<typeof getBundledModels>[0],
|
||||
);
|
||||
return {
|
||||
providerId: "novita",
|
||||
fetchDynamicModels: async () =>
|
||||
fetchOpenAICompatibleModels({
|
||||
api: "openai-completions",
|
||||
provider: "novita",
|
||||
baseUrl,
|
||||
apiKey,
|
||||
mapModel: (entry, defaults) => mapNovitaModel(entry, defaults, references.get(defaults.id)),
|
||||
filterModel: (entry, model) => {
|
||||
const active = typeof entry.status !== "number" || entry.status === 1;
|
||||
return (
|
||||
active &&
|
||||
isPublicNovitaModelId(model.id) &&
|
||||
novitaArrayIncludes(entry.endpoints, "chat/completions") &&
|
||||
model.maxTokens !== 0
|
||||
);
|
||||
},
|
||||
fetch: config?.fetch,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 6. xAI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { getOAuthProviders } from "@oh-my-pi/pi-ai/registry/oauth";
|
||||
import { getEnvApiKey } from "@oh-my-pi/pi-ai/stream";
|
||||
import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
|
||||
import { novitaModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
|
||||
|
||||
describe("Novita built-in provider", () => {
|
||||
test("registers catalog descriptor with NOVITA_API_KEY env discovery", () => {
|
||||
const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === "novita");
|
||||
expect(descriptor).toBeDefined();
|
||||
expect(descriptor?.defaultModel).toBe("moonshotai/kimi-k2.7-code");
|
||||
expect(descriptor?.catalogDiscovery?.envVars).toContain("NOVITA_API_KEY");
|
||||
expect(descriptor?.catalogDiscovery?.allowUnauthenticated).toBe(true);
|
||||
expect(DEFAULT_MODEL_PER_PROVIDER.novita).toBe("moonshotai/kimi-k2.7-code");
|
||||
});
|
||||
|
||||
test("registers Novita as an API-key login provider", () => {
|
||||
const provider = getOAuthProviders().find(item => item.id === "novita");
|
||||
expect(provider?.name).toBe("Novita");
|
||||
expect(provider?.available).toBe(true);
|
||||
});
|
||||
|
||||
test("resolves NOVITA_API_KEY via env", () => {
|
||||
const previous = Bun.env.NOVITA_API_KEY;
|
||||
Bun.env.NOVITA_API_KEY = "novita-test-key";
|
||||
try {
|
||||
expect(getEnvApiKey("novita")).toBe("novita-test-key");
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete Bun.env.NOVITA_API_KEY;
|
||||
} else {
|
||||
Bun.env.NOVITA_API_KEY = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("maps Novita model catalog metadata from the public OpenAI-compatible endpoint", async () => {
|
||||
const requests: string[] = [];
|
||||
const fetchMock = async (input: string | URL | Request): Promise<Response> => {
|
||||
requests.push(input.toString());
|
||||
return Response.json({
|
||||
data: [
|
||||
{
|
||||
id: "moonshotai/kimi-k2.7-code",
|
||||
display_name: "Kimi K2.7 Code",
|
||||
status: 1,
|
||||
context_size: 262144,
|
||||
max_output_tokens: 131072,
|
||||
input_token_price_per_m: 9500,
|
||||
output_token_price_per_m: 40000,
|
||||
pricing: {
|
||||
input_cache_read: {
|
||||
price_per_m: 1900,
|
||||
},
|
||||
},
|
||||
features: ["serverless", "function-calling", "structured-outputs", "reasoning"],
|
||||
endpoints: ["chat/completions", "anthropic"],
|
||||
input_modalities: ["text", "image", "video"],
|
||||
},
|
||||
{
|
||||
id: "qwen/qwen3-8b-fp8",
|
||||
status: 4,
|
||||
context_size: 128000,
|
||||
max_output_tokens: 20000,
|
||||
endpoints: ["chat/completions"],
|
||||
input_modalities: ["text"],
|
||||
},
|
||||
{
|
||||
id: "ai_infer_test_1",
|
||||
status: 1,
|
||||
context_size: 200000,
|
||||
max_output_tokens: 200000,
|
||||
features: ["function-calling"],
|
||||
endpoints: ["chat/completions"],
|
||||
input_modalities: ["text"],
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const options = novitaModelManagerOptions({ fetch: fetchMock });
|
||||
const models = await options.fetchDynamicModels?.();
|
||||
const model = models?.find(item => item.id === "moonshotai/kimi-k2.7-code");
|
||||
|
||||
expect(requests).toEqual(["https://api.novita.ai/openai/v1/models"]);
|
||||
expect(models?.map(item => item.id)).toEqual(["moonshotai/kimi-k2.7-code"]);
|
||||
expect(model?.provider).toBe("novita");
|
||||
expect(model?.baseUrl).toBe("https://api.novita.ai/openai/v1");
|
||||
expect(model?.name).toBe("Kimi K2.7 Code");
|
||||
expect(model?.reasoning).toBe(true);
|
||||
expect(model?.supportsTools).toBe(true);
|
||||
expect(model?.input).toEqual(["text", "image"]);
|
||||
expect(model?.cost).toEqual({ input: 9.5, output: 40, cacheRead: 1.9, cacheWrite: 0 });
|
||||
expect(model?.contextWindow).toBe(262144);
|
||||
expect(model?.maxTokens).toBe(131072);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user