Add Novita AI integration to @supermemory/tools

Adds a dedicated Novita AI provider integration following the OpenAI SDK pattern:
- New novita submodule with withSupermemory middleware
- Supports OpenAI-compatible endpoint (https://api.novita.ai/openai)
- Uses NOVITA_API_KEY env var for authentication
- Exports NOVITA_MODELS constants with model IDs (kimi-k2.5, glm-5, minimax-m2.5)
- Includes unit tests for the integration
This commit is contained in:
Alex-wuhu 2026-03-24 20:02:44 +08:00
parent fc94dd8308
commit 89117a9d68
6 changed files with 671 additions and 0 deletions

View file

@ -1,3 +1,18 @@
export type { SupermemoryToolsConfig } from "./types"
export type { OpenAIMiddlewareOptions } from "./openai"
export type {
NovitaMiddlewareOptions,
NovitaClientOptions,
NovitaModel,
NovitaEmbeddingModel,
} from "./novita"
export {
NOVITA_MODELS,
NOVITA_EMBEDDING_MODELS,
NOVITA_ENDPOINTS,
withSupermemory as withSupermemoryNovita,
wrapNovitaClient,
createNovita,
} from "./novita"

View file

@ -0,0 +1,78 @@
import type OpenAI from "openai"
import { createNovitaClient, createNovitaMiddleware } from "./middleware"
import {
NOVITA_MODELS,
NOVITA_EMBEDDING_MODELS,
NOVITA_ENDPOINTS,
type NovitaMiddlewareOptions,
type NovitaClientOptions,
type NovitaModel,
type NovitaEmbeddingModel,
} from "./types"
export {
createSearchMemoriesFunction,
createAddMemoryFunction,
createGetProfileFunction,
createDocumentListFunction,
createDocumentDeleteFunction,
createDocumentAddFunction,
createMemoryForgetFunction,
supermemoryTools,
getToolDefinitions,
createToolCallExecutor,
createToolCallsExecutor,
createSearchMemoriesTool,
createAddMemoryTool,
createGetProfileTool,
createDocumentListTool,
createDocumentDeleteTool,
createDocumentAddTool,
createMemoryForgetTool,
memoryToolSchemas,
type MemorySearchResult,
type MemoryAddResult,
type ProfileResult,
type DocumentListResult,
type DocumentDeleteResult,
type DocumentAddResult,
type MemoryForgetResult,
} from "../openai/tools"
export type {
NovitaMiddlewareOptions as WithSupermemoryOptions,
NovitaClientOptions,
NovitaModel,
NovitaEmbeddingModel,
}
export { NOVITA_MODELS, NOVITA_EMBEDDING_MODELS, NOVITA_ENDPOINTS }
export function withSupermemory(
containerTag: string,
options?: NovitaMiddlewareOptions,
clientOptions?: NovitaClientOptions,
): OpenAI {
if (!process.env.SUPERMEMORY_API_KEY) {
throw new Error("SUPERMEMORY_API_KEY is not set")
}
const novitaClient = createNovitaClient(clientOptions)
return createNovitaMiddleware(novitaClient, containerTag, options)
}
export function wrapNovitaClient(
novitaClient: OpenAI,
containerTag: string,
options?: NovitaMiddlewareOptions,
): OpenAI {
if (!process.env.SUPERMEMORY_API_KEY) {
throw new Error("SUPERMEMORY_API_KEY is not set")
}
return createNovitaMiddleware(novitaClient, containerTag, options)
}
export function createNovita(options?: NovitaClientOptions): OpenAI {
return createNovitaClient(options)
}

View file

@ -0,0 +1,331 @@
import OpenAI from "openai"
import {
NOVITA_ENDPOINTS,
type NovitaClientOptions,
type NovitaMiddlewareOptions,
} from "./types"
const NOVITA_API_KEY = process.env.NOVITA_API_KEY
export function createNovitaClient(options?: NovitaClientOptions): OpenAI {
const apiKey = options?.apiKey ?? NOVITA_API_KEY
if (!apiKey) {
throw new Error(
"NOVITA_API_KEY is not set — provide it via `options.apiKey` or set `process.env.NOVITA_API_KEY`",
)
}
return new OpenAI({
apiKey,
baseURL: options?.baseURL ?? NOVITA_ENDPOINTS.OPENAI,
...(options?.organization && { organization: options.organization }),
})
}
export function createNovitaMiddleware(
novitaClient: OpenAI,
containerTag: string,
options?: NovitaMiddlewareOptions,
): OpenAI {
const logger = {
info: (message: string, data?: Record<string, unknown>) => {
if (options?.verbose) {
console.log(`[novita-supermemory] ${message}`, data ?? "")
}
},
error: (message: string, data?: Record<string, unknown>) => {
console.error(`[novita-supermemory] ${message}`, data ?? "")
},
debug: (message: string, data?: Record<string, unknown>) => {
if (options?.verbose) {
console.debug(`[novita-supermemory] ${message}`, data ?? "")
}
},
}
const conversationId = options?.conversationId
const mode = options?.mode ?? "profile"
const addMemory = options?.addMemory ?? "never"
const baseUrl = options?.baseUrl ?? "https://api.supermemory.ai"
const originalCreate = novitaClient.chat.completions.create
const originalResponsesCreate = novitaClient.responses?.create
const getLastUserMessage = (
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
) => {
const lastUserMessage = messages
.slice()
.reverse()
.find((msg) => msg.role === "user")
return typeof lastUserMessage?.content === "string"
? lastUserMessage.content
: ""
}
const supermemoryProfileSearch = async (
containerTag: string,
queryText: string,
): Promise<{
profile: {
static?: Array<{ memory: string; metadata?: Record<string, unknown> }>
dynamic?: Array<{ memory: string; metadata?: Record<string, unknown> }>
}
searchResults: {
results: Array<{ memory: string; metadata?: Record<string, unknown> }>
}
}> => {
const payload = queryText
? JSON.stringify({
q: queryText,
containerTag: containerTag,
})
: JSON.stringify({
containerTag: containerTag,
})
const response = await fetch(`${baseUrl}/v4/profile`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
},
body: payload,
})
if (!response.ok) {
const errorText = await response.text().catch(() => "Unknown error")
throw new Error(
`Supermemory profile search failed: ${response.status} ${response.statusText}. ${errorText}`,
)
}
return await response.json()
}
const convertProfileToMarkdown = (data: {
profile: {
static?: Array<{ memory: string }>
dynamic?: Array<{ memory: string }>
}
searchResults: { results: Array<{ memory: string }> }
}): string => {
const parts: string[] = []
if (data.profile.static?.length) {
parts.push("### Static Profile (Facts about user)")
for (const item of data.profile.static) {
parts.push(`- ${item.memory}`)
}
}
if (data.profile.dynamic?.length) {
parts.push("### Dynamic Context (Recent activity)")
for (const item of data.profile.dynamic) {
parts.push(`- ${item.memory}`)
}
}
return parts.join("\n")
}
const deduplicateMemories = (data: {
static?: Array<{ memory: string }>
dynamic?: Array<{ memory: string }>
searchResults?: Array<{ memory: string }>
}): {
static: string[]
dynamic: string[]
searchResults: string[]
} => {
const seen = new Set<string>()
const result = {
static: [] as string[],
dynamic: [] as string[],
searchResults: [] as string[],
}
for (const item of data.static ?? []) {
const normalized = item.memory.toLowerCase().trim()
if (!seen.has(normalized)) {
seen.add(normalized)
result.static.push(item.memory)
}
}
for (const item of data.dynamic ?? []) {
const normalized = item.memory.toLowerCase().trim()
if (!seen.has(normalized)) {
seen.add(normalized)
result.dynamic.push(item.memory)
}
}
for (const item of data.searchResults ?? []) {
const normalized = item.memory.toLowerCase().trim()
if (!seen.has(normalized)) {
seen.add(normalized)
result.searchResults.push(item.memory)
}
}
return result
}
const addSystemPrompt = async (
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
containerTag: string,
mode: "profile" | "query" | "full",
): Promise<OpenAI.Chat.Completions.ChatCompletionMessageParam[]> => {
const systemPromptExists = messages.some((msg) => msg.role === "system")
const queryText = mode !== "profile" ? getLastUserMessage(messages) : ""
const memoriesResponse = await supermemoryProfileSearch(
containerTag,
queryText,
)
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
logger.info("Memory search completed", {
containerTag,
memoryCountStatic,
memoryCountDynamic,
mode,
})
const deduplicated = deduplicateMemories({
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults?.results,
})
const profileData =
mode !== "query"
? convertProfileToMarkdown({
profile: {
static: deduplicated.static.map((m) => ({ memory: m })),
dynamic: deduplicated.dynamic.map((m) => ({ memory: m })),
},
searchResults: { results: [] },
})
: ""
const searchResultsMemories =
mode !== "profile"
? `Search results for user's recent message: \n${deduplicated.searchResults.map((memory) => `- ${memory}`).join("\n")}`
: ""
const memories = `${profileData}\n${searchResultsMemories}`.trim()
if (!memories) {
return messages
}
if (systemPromptExists) {
return messages.map((msg) =>
msg.role === "system"
? { ...msg, content: `${msg.content} \n ${memories}` }
: msg,
)
}
return [{ role: "system" as const, content: memories }, ...messages]
}
const createWithMemory = async (
params: OpenAI.Chat.Completions.ChatCompletionCreateParams,
) => {
const messages = Array.isArray(params.messages) ? params.messages : []
if (mode !== "profile") {
const userMessage = getLastUserMessage(messages)
if (!userMessage) {
logger.debug("No user message found, skipping memory search")
return originalCreate.call(novitaClient.chat.completions, params)
}
}
logger.info("Starting memory search", {
containerTag,
conversationId,
mode,
})
const enhancedMessages = await addSystemPrompt(messages, containerTag, mode)
return originalCreate.call(novitaClient.chat.completions, {
...params,
messages: enhancedMessages,
})
}
novitaClient.chat.completions.create =
createWithMemory as typeof originalCreate
if (originalResponsesCreate) {
const createResponsesWithMemory = async (
params: Parameters<typeof originalResponsesCreate>[0],
) => {
const input = typeof params.input === "string" ? params.input : ""
if (mode !== "profile" && !input) {
logger.debug("No input found for Responses API, skipping memory search")
return originalResponsesCreate.call(novitaClient.responses, params)
}
logger.info("Starting memory search for Responses API", {
containerTag,
conversationId,
mode,
})
const queryText = mode !== "profile" ? input : ""
const memoriesResponse = await supermemoryProfileSearch(
containerTag,
queryText,
)
const deduplicated = deduplicateMemories({
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults?.results,
})
const profileData =
mode !== "query"
? convertProfileToMarkdown({
profile: {
static: deduplicated.static.map((m) => ({ memory: m })),
dynamic: deduplicated.dynamic.map((m) => ({ memory: m })),
},
searchResults: { results: [] },
})
: ""
const searchResultsMemories =
mode !== "profile"
? `Search results: \n${deduplicated.searchResults.map((memory) => `- ${memory}`).join("\n")}`
: ""
const memories = `${profileData}\n${searchResultsMemories}`.trim()
const enhancedInstructions = memories
? `${params.instructions || ""}\n\n${memories}`.trim()
: params.instructions
return originalResponsesCreate.call(novitaClient.responses, {
...params,
instructions: enhancedInstructions,
})
}
novitaClient.responses.create =
createResponsesWithMemory as typeof originalResponsesCreate
}
return novitaClient
}

View file

@ -0,0 +1,86 @@
import type OpenAI from "openai"
/**
* Novita AI configuration options
*/
export interface NovitaMiddlewareOptions {
/** Optional conversation ID to group messages for contextual memory generation */
conversationId?: string
/** Enable detailed logging of memory operations (default: false) */
verbose?: boolean
/**
* Memory retrieval mode:
* - "profile": Retrieves user profile memories (static + dynamic) without query filtering
* - "query": Searches memories based on semantic similarity to the user's message
* - "full": Combines both profile and query-based results
*/
mode?: "profile" | "query" | "full"
/**
* Memory persistence mode:
* - "always": Automatically save conversations as memories
* - "never": Only retrieve memories, don't store new ones
*/
addMemory?: "always" | "never"
/** Custom Supermemory API base URL */
baseUrl?: string
}
/**
* Novita AI client options
*/
export interface NovitaClientOptions {
/** Novita API key (falls back to NOVITA_API_KEY env var) */
apiKey?: string
/** Custom base URL (default: https://api.novita.ai/openai) */
baseURL?: string
/** Organization ID */
organization?: string
}
/**
* Available Novita AI models
* @see https://novita.ai/models
*/
export const NOVITA_MODELS = {
/**
* Default model - MoE architecture with function calling, structured output, reasoning, and vision
* Context: 262,144 tokens | Max Output: 262,144 tokens
*/
DEFAULT: "moonshotai/kimi-k2.5",
/**
* GLM-5 - MoE architecture with function calling, structured output, reasoning
* Context: 202,800 tokens | Max Output: 131,072 tokens
*/
GLM_5: "zai-org/glm-5",
/**
* MiniMax M2.5 - MoE architecture with function calling, structured output, reasoning
* Context: 204,800 tokens | Max Output: 131,100 tokens
*/
MINIMAX_M2_5: "minimax/minimax-m2.5",
} as const
/**
* Novita AI embedding models
*/
export const NOVITA_EMBEDDING_MODELS = {
/**
* Qwen3 Embedding - 1024 dimensions, max 8,192 input tokens
*/
DEFAULT: "qwen/qwen3-embedding-0.6b",
} as const
/**
* Novita AI API endpoints
*/
export const NOVITA_ENDPOINTS = {
/** OpenAI-compatible endpoint */
OPENAI: "https://api.novita.ai/openai",
/** Anthropic-compatible endpoint */
ANTHROPIC: "https://api.novita.ai/anthropic",
} as const
export type NovitaModel = (typeof NOVITA_MODELS)[keyof typeof NOVITA_MODELS]
export type NovitaEmbeddingModel =
(typeof NOVITA_EMBEDDING_MODELS)[keyof typeof NOVITA_EMBEDDING_MODELS]

View file

@ -0,0 +1,160 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
import {
withSupermemory,
wrapNovitaClient,
createNovita,
NOVITA_MODELS,
NOVITA_ENDPOINTS,
} from "../../src/novita"
import OpenAI from "openai"
import "dotenv/config"
const TEST_CONFIG = {
apiKey: process.env.SUPERMEMORY_API_KEY || "test-api-key",
novitaApiKey: process.env.NOVITA_API_KEY || "test-novita-key",
baseURL: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
containerTag: "test-novita-wrapper",
}
describe("Unit: Novita Integration", () => {
let originalEnv: string | undefined
let originalFetch: typeof globalThis.fetch
beforeEach(() => {
originalEnv = process.env.SUPERMEMORY_API_KEY
originalFetch = globalThis.fetch
vi.clearAllMocks()
})
afterEach(() => {
if (originalEnv) {
process.env.SUPERMEMORY_API_KEY = originalEnv
} else {
delete process.env.SUPERMEMORY_API_KEY
}
globalThis.fetch = originalFetch
})
describe("Constants and Exports", () => {
it("should export NOVITA_MODELS with correct model IDs", () => {
expect(NOVITA_MODELS.DEFAULT).toBe("moonshotai/kimi-k2.5")
expect(NOVITA_MODELS.GLM_5).toBe("zai-org/glm-5")
expect(NOVITA_MODELS.MINIMAX_M2_5).toBe("minimax/minimax-m2.5")
})
it("should export NOVITA_ENDPOINTS with correct URLs", () => {
expect(NOVITA_ENDPOINTS.OPENAI).toBe("https://api.novita.ai/openai")
expect(NOVITA_ENDPOINTS.ANTHROPIC).toBe("https://api.novita.ai/anthropic")
})
})
describe("Environment validation", () => {
it("should throw error if SUPERMEMORY_API_KEY is not set", () => {
delete process.env.SUPERMEMORY_API_KEY
expect(() => {
withSupermemory(TEST_CONFIG.containerTag)
}).toThrow("SUPERMEMORY_API_KEY is not set")
})
it("should throw error if NOVITA_API_KEY is not set when creating client", () => {
delete process.env.NOVITA_API_KEY
expect(() => {
createNovita()
}).toThrow("NOVITA_API_KEY is not set")
})
it("should successfully create client with valid API keys", () => {
process.env.SUPERMEMORY_API_KEY = "test-key"
process.env.NOVITA_API_KEY = "test-novita-key"
const client = createNovita()
expect(client).toBeDefined()
expect(client).toBeInstanceOf(OpenAI)
})
it("should successfully wrap client with supermemory", () => {
process.env.SUPERMEMORY_API_KEY = "test-key"
process.env.NOVITA_API_KEY = "test-novita-key"
const client = createNovita()
const wrappedClient = wrapNovitaClient(client, TEST_CONFIG.containerTag)
expect(wrappedClient).toBeDefined()
})
it("should successfully create wrapped client in one call", () => {
process.env.SUPERMEMORY_API_KEY = "test-key"
process.env.NOVITA_API_KEY = "test-novita-key"
const wrappedClient = withSupermemory(TEST_CONFIG.containerTag)
expect(wrappedClient).toBeDefined()
})
})
describe("Client configuration", () => {
it("should use custom baseURL when provided", () => {
process.env.NOVITA_API_KEY = "test-key"
const client = createNovita({ baseURL: "https://custom.api.com" })
expect(client).toBeDefined()
})
it("should use default Novita endpoint when baseURL not provided", () => {
process.env.NOVITA_API_KEY = "test-key"
const client = createNovita()
expect(client.baseURL).toBe(NOVITA_ENDPOINTS.OPENAI)
})
it("should accept apiKey in options", () => {
const client = createNovita({ apiKey: "custom-key" })
expect(client).toBeDefined()
})
})
describe("withSupermemory options", () => {
it("should accept verbose option", () => {
process.env.SUPERMEMORY_API_KEY = "test-key"
process.env.NOVITA_API_KEY = "test-key"
const wrappedClient = withSupermemory(TEST_CONFIG.containerTag, {
verbose: true,
})
expect(wrappedClient).toBeDefined()
})
it("should accept mode option", () => {
process.env.SUPERMEMORY_API_KEY = "test-key"
process.env.NOVITA_API_KEY = "test-key"
const modes = ["profile", "query", "full"] as const
for (const mode of modes) {
const wrappedClient = withSupermemory(TEST_CONFIG.containerTag, {
mode,
})
expect(wrappedClient).toBeDefined()
}
})
it("should accept addMemory option", () => {
process.env.SUPERMEMORY_API_KEY = "test-key"
process.env.NOVITA_API_KEY = "test-key"
const wrappedClient = withSupermemory(TEST_CONFIG.containerTag, {
addMemory: "always",
})
expect(wrappedClient).toBeDefined()
})
it("should accept conversationId option", () => {
process.env.SUPERMEMORY_API_KEY = "test-key"
process.env.NOVITA_API_KEY = "test-key"
const wrappedClient = withSupermemory(TEST_CONFIG.containerTag, {
conversationId: "conv-123",
})
expect(wrappedClient).toBeDefined()
})
})
})

View file

@ -7,6 +7,7 @@ export default defineConfig({
"src/claude-memory.ts",
"src/openai/index.ts",
"src/mastra.ts",
"src/novita/index.ts",
],
format: "esm",
sourcemap: false,