native tool calling detection for novita

This commit is contained in:
Timothy Carambat 2026-03-05 10:19:03 -08:00
parent 0e9dc6572b
commit ee4b208f95
2 changed files with 131 additions and 21 deletions

View File

@ -82,7 +82,7 @@ class NovitaLLM {
// from the current date. If it is, then we will refetch the API so that all the models are up // from the current date. If it is, then we will refetch the API so that all the models are up
// to date. // to date.
#cacheIsStale() { #cacheIsStale() {
const MAX_STALE = 6.048e8; // 1 Week in MS const MAX_STALE = 2.592e8; // 3 days in MS
if (!fs.existsSync(this.cacheAtPath)) return true; if (!fs.existsSync(this.cacheAtPath)) return true;
const now = Number(new Date()); const now = Number(new Date());
const timestampMs = Number(fs.readFileSync(this.cacheAtPath)); const timestampMs = Number(fs.readFileSync(this.cacheAtPath));
@ -143,6 +143,32 @@ class NovitaLLM {
return availableModels[this.model]?.maxLength || 4096; return availableModels[this.model]?.maxLength || 4096;
} }
/**
* Get the capabilities of a model from the Novita API.
* @returns {Promise<{tools: 'unknown' | boolean, reasoning: 'unknown' | boolean, imageGeneration: 'unknown' | boolean, vision: 'unknown' | boolean}>}
*/
async getModelCapabilities() {
try {
await this.#syncModels();
const availableModels = this.models();
const modelInfo = availableModels[this.model];
return {
tools: modelInfo.features.includes("function-calling"),
reasoning: modelInfo.features.includes("reasoning"),
imageGeneration: false, // no image generation capabilities for Novita yet.
vision: modelInfo.input_modalities.includes("image"),
};
} catch (error) {
console.error("Error getting model capabilities:", error);
return {
tools: "unknown",
reasoning: "unknown",
imageGeneration: "unknown",
vision: "unknown",
};
}
}
async isValidChatCompletionModel(model = "") { async isValidChatCompletionModel(model = "") {
await this.#syncModels(); await this.#syncModels();
const availableModels = this.models(); const availableModels = this.models();
@ -398,6 +424,8 @@ async function fetchNovitaModels() {
model.id.split("/")[0].charAt(0).toUpperCase() + model.id.split("/")[0].charAt(0).toUpperCase() +
model.id.split("/")[0].slice(1), model.id.split("/")[0].slice(1),
maxLength: model.context_size, maxLength: model.context_size,
features: model.features ?? [],
input_modalities: model.input_modalities ?? [],
}; };
}); });

View File

@ -2,9 +2,14 @@ const OpenAI = require("openai");
const Provider = require("./ai-provider.js"); const Provider = require("./ai-provider.js");
const InheritMultiple = require("./helpers/classes.js"); const InheritMultiple = require("./helpers/classes.js");
const UnTooled = require("./helpers/untooled.js"); const UnTooled = require("./helpers/untooled.js");
const { tooledStream, tooledComplete } = require("./helpers/tooled.js");
const { RetryError } = require("../error.js");
const { NovitaLLM } = require("../../../AiProviders/novita/index.js");
/** /**
* The agent provider for the Novita AI provider. * The agent provider for the Novita AI provider.
* Supports true OpenAI-compatible tool calling when the model supports it,
* falling back to the UnTooled prompt-based approach otherwise.
*/ */
class NovitaProvider extends InheritMultiple([Provider, UnTooled]) { class NovitaProvider extends InheritMultiple([Provider, UnTooled]) {
model; model;
@ -25,8 +30,13 @@ class NovitaProvider extends InheritMultiple([Provider, UnTooled]) {
this._client = client; this._client = client;
this.model = model; this.model = model;
this.verbose = true; this.verbose = true;
this._supportsToolCalling = null;
} }
/**
* Get the Novita client.
* @returns {import("openai").OpenAI}
*/
get client() { get client() {
return this._client; return this._client;
} }
@ -36,12 +46,16 @@ class NovitaProvider extends InheritMultiple([Provider, UnTooled]) {
} }
/** /**
* Whether this provider supports native OpenAI-compatible tool calling. * Whether the loaded model supports native OpenAI-compatible tool calling.
* Override in subclass and return true to use native tool calling instead of UnTooled. * Checks the Novita model capabilities and caches the result.
* @returns {boolean|Promise<boolean>} * @returns {Promise<boolean>}
*/ */
supportsNativeToolCalling() { async supportsNativeToolCalling() {
return false; if (this._supportsToolCalling !== null) return this._supportsToolCalling;
const novita = new NovitaLLM(null, this.model);
const capabilities = await novita.getModelCapabilities();
this._supportsToolCalling = capabilities.tools === true;
return this._supportsToolCalling;
} }
async #handleFunctionCallChat({ messages = [] }) { async #handleFunctionCallChat({ messages = [] }) {
@ -70,33 +84,101 @@ class NovitaProvider extends InheritMultiple([Provider, UnTooled]) {
}); });
} }
/**
* Stream a chat completion with tool calling support.
* Uses native tool calling when supported, otherwise falls back to UnTooled.
*/
async stream(messages, functions = [], eventHandler = null) { async stream(messages, functions = [], eventHandler = null) {
return await UnTooled.prototype.stream.call( const useNative =
this, functions.length > 0 && (await this.supportsNativeToolCalling());
messages,
functions, if (!useNative) {
this.#handleFunctionCallStream.bind(this), return await UnTooled.prototype.stream.call(
eventHandler this,
messages,
functions,
this.#handleFunctionCallStream.bind(this),
eventHandler
);
}
this.providerLog(
"Provider.stream (tooled) - will process this chat completion."
); );
try {
return await tooledStream(
this.client,
this.model,
messages,
functions,
eventHandler
);
} catch (error) {
console.error(error.message, error);
if (error instanceof OpenAI.AuthenticationError) throw error;
if (
error instanceof OpenAI.RateLimitError ||
error instanceof OpenAI.InternalServerError ||
error instanceof OpenAI.APIError
) {
throw new RetryError(error.message);
}
throw error;
}
} }
/**
* Create a non-streaming completion with tool calling support.
* Uses native tool calling when supported, otherwise falls back to UnTooled.
*/
async complete(messages, functions = []) { async complete(messages, functions = []) {
return await UnTooled.prototype.complete.call( const useNative =
this, functions.length > 0 && (await this.supportsNativeToolCalling());
messages,
functions, if (!useNative) {
this.#handleFunctionCallChat.bind(this) return await UnTooled.prototype.complete.call(
); this,
messages,
functions,
this.#handleFunctionCallChat.bind(this)
);
}
try {
const result = await tooledComplete(
this.client,
this.model,
messages,
functions,
this.getCost.bind(this)
);
if (result.retryWithError) {
return this.complete([...messages, result.retryWithError], functions);
}
return result;
} catch (error) {
if (error instanceof OpenAI.AuthenticationError) throw error;
if (
error instanceof OpenAI.RateLimitError ||
error instanceof OpenAI.InternalServerError ||
error instanceof OpenAI.APIError
) {
throw new RetryError(error.message);
}
throw error;
}
} }
/** /**
* Get the cost of the completion. * Get the cost of the completion.
* * Stubbed since Novita AI has no cost basis.
* @param _usage The completion to get the cost for. * @param _usage The completion to get the cost for.
* @returns The cost of the completion. * @returns The cost of the completion.
* Stubbed since Novita AI has no cost basis.
*/ */
getCost() { getCost(_usage) {
return 0; return 0;
} }
} }