@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
169 lines (168 loc) • 6.29 kB
JavaScript
/**
* Model detection utilities for capability checking
*/
import { IMAGE_GENERATION_MODELS } from "../core/constants.js";
/**
* Check if model name is valid for detection functions
*/
function isValidModelName(modelName) {
return typeof modelName === "string" && modelName.length > 0;
}
export function isGemini3Model(modelName) {
if (!isValidModelName(modelName)) {
return false;
}
return /^gemini-3(\.\d+)?(-.*)?$/i.test(modelName);
}
export function isGemini25Model(modelName) {
if (!isValidModelName(modelName)) {
return false;
}
return /^gemini-2\.5(-.*)?$/i.test(modelName);
}
export function supportsThinkingConfig(modelName) {
if (!isValidModelName(modelName)) {
return false;
}
const thinkingModels = [
/^gemini-3/i,
/^gemini-2\.5-pro/i,
/^gemini-2\.5-flash/i,
];
return thinkingModels.some((pattern) => pattern.test(modelName));
}
export function supportsPromptCaching(modelName) {
if (!isValidModelName(modelName)) {
return false;
}
const cachingModels = [
/^gemini-3/i,
/^gemini-2\.5/i,
/^gpt-4/i,
/^claude-3/i,
];
return cachingModels.some((pattern) => pattern.test(modelName));
}
export function getMaxThinkingBudgetTokens(modelName) {
if (!isValidModelName(modelName)) {
return 10000;
}
if (/^gemini-3(\.\d+)?-pro/i.test(modelName)) {
return 100000;
}
if (/^gemini-3(\.\d+)?-flash/i.test(modelName)) {
return 50000;
}
if (/^gemini-2\.5/i.test(modelName)) {
return 32000;
}
return 10000;
}
// Per-model `thinkingBudget` bounds for the Gemini 2.5 family, confirmed
// against live Vertex generateContent responses (not just vendor docs, which
// have been observed to drift from runtime behavior — see the "known gotcha"
// on Gemini 2.5 Pro's range in public bug reports). Vertex rejects a budget
// outside these bounds with INVALID_ARGUMENT "thinking_budget is out of
// range; supported values are integers from <min> to <max>".
//
// Flash's true floor is 1, but budget:1 measured 0 thoughtsTokenCount — i.e.
// technically accepted but indistinguishable from thinking being off. The
// floor is bumped to 128 (Pro's real floor) here so a "minimal" mapping
// still produces observable thinking. Must precede the plain "flash" pattern
// below — "gemini-2.5-flash-lite" also matches a bare `/^gemini-2\.5-flash/`.
const GEMINI_25_THINKING_BUDGET_RANGES = [
{ pattern: /^gemini-2\.5-flash-lite/i, min: 512, max: 24576 },
{ pattern: /^gemini-2\.5-flash/i, min: 128, max: 24576 },
{ pattern: /^gemini-2\.5-pro/i, min: 128, max: 32768 },
];
// Bounds safe for any Gemini 2.5 variant not explicitly listed above — the
// intersection of all three known models' verified ranges, so a model we
// haven't named still gets a budget every family member accepts.
const GEMINI_25_FALLBACK_BUDGET_RANGE = { min: 512, max: 24576 };
/**
* Resolves the Vertex-accepted `thinkingBudget` bounds for a Gemini 2.5
* model. Returns undefined for anything that isn't a Gemini 2.5 model —
* Gemini 3 takes `thinkingLevel` directly, and no thinking-budget mapping
* has been verified for earlier Gemini generations.
*/
export function getGemini25ThinkingBudgetRange(modelName) {
if (!isValidModelName(modelName) || !isGemini25Model(modelName)) {
return undefined;
}
for (const { pattern, min, max } of GEMINI_25_THINKING_BUDGET_RANGES) {
if (pattern.test(modelName)) {
return { min, max };
}
}
return GEMINI_25_FALLBACK_BUDGET_RANGE;
}
export function getModelFamily(modelName) {
if (!isValidModelName(modelName)) {
return "unknown";
}
if (/^gemini-3/i.test(modelName)) {
return "gemini-3";
}
if (/^gemini-2\.5/i.test(modelName)) {
return "gemini-2.5";
}
if (/^gemini-2/i.test(modelName)) {
return "gemini-2";
}
if (/^gpt-4/i.test(modelName)) {
return "gpt-4";
}
if (/^claude-3/i.test(modelName)) {
return "claude-3";
}
return "unknown";
}
// Compiled once at module load — hasRestrictedOutputLimit() is called per
// request and previously rebuilt this array on every call.
const IMAGE_MODEL_PATTERNS = IMAGE_GENERATION_MODELS.map((m) => new RegExp(`^${m.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"));
/**
* Check if a model has restricted output token limit (32768 max)
* This applies to:
* - All Gemini 3 models (gemini-3-flash, gemini-3-pro, etc.)
* - Image generation models (gemini-2.5-flash-image, gemini-3-pro-image-preview)
*/
export function hasRestrictedOutputLimit(modelName) {
if (!isValidModelName(modelName)) {
return false;
}
// Check for Gemini 3 models (anchored regex for consistency)
if (/^gemini-3/i.test(modelName)) {
return true;
}
if (IMAGE_MODEL_PATTERNS.some((pattern) => pattern.test(modelName))) {
return true;
}
return false;
}
/**
* Get the max output tokens for a model (32768 for restricted models)
*/
export const RESTRICTED_OUTPUT_TOKEN_LIMIT = 32768;
/**
* Normalize an Anthropic-API-style Claude model ID to the Vertex publisher
* format.
*
* The Anthropic API dates models with a trailing dash segment
* ("claude-haiku-4-5-20251001") while Vertex publisher IDs separate the date
* with "@" ("claude-haiku-4-5@20251001"). Vertex rejects the dash form with a
* 404 (verified live against us-east5), so the native Vertex+Claude paths
* normalize before calling @anthropic-ai/vertex-sdk.
*
* Pass-through cases: IDs already in "@" form, bare aliases with no date
* ("claude-sonnet-4-6" — Vertex resolves these itself), and non-Claude models.
* Legacy v2-suffixed Vertex IDs ("claude-3-5-sonnet-v2@20241022") have no
* dash-date equivalent, so those legacy dash IDs stay out of scope: they 404
* today and still 404 after the transform — no regression either way.
*/
export function toVertexAnthropicModelId(modelName) {
if (!modelName.startsWith("claude-") || modelName.includes("@")) {
return modelName;
}
const dashDate = modelName.match(/^(claude-[a-z0-9-]+)-(\d{8})$/);
return dashDate ? `${dashDate[1]}@${dashDate[2]}` : modelName;
}