@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
172 lines (171 loc) • 8.61 kB
JavaScript
/**
* Policy for when AI-SDK structured output (experimental_output) must be
* disabled because the provider cannot combine tool calls with JSON-schema
* enforcement.
*
* Two provider surfaces have this conflict:
* - Gemini (google-ai, or Vertex with a non-Claude model).
* - The native Anthropic Messages API surface (provider "anthropic"/"bedrock",
* including via a proxy/base-URL override). experimental_output silently
* drops tool_use blocks when tools are also present (finishReason=tool-calls
* but zero parsed tool calls), so structured output must be disabled there too.
*
* Vertex+Claude (provider "vertex", modelName starts with "claude-") uses a
* different transport that supports both simultaneously and must NOT be excluded —
* a gate keyed on "any Vertex model" wrongly disables it for the primary
* production config and forces fragile hand-parsed JSON.
*/
/** True when the provider+model is a Gemini model (the only family with the tools↔schema conflict). */
export function isGeminiProvider(providerName, modelName) {
if (providerName === "google-ai") {
return true;
}
if (providerName === "vertex") {
// Vertex hosts both Gemini and Claude. Only non-Claude (Gemini) models
// have the tools↔schema conflict.
return !(modelName?.startsWith("claude-") ?? false);
}
return false;
}
/**
* True when the provider is the native Anthropic Messages API surface
* (provider "anthropic" — including via a proxy/base-URL override — or "bedrock").
* experimental_output + tools silently drops tool_use blocks on this surface, so
* structured output must be disabled when tools are active. Vertex+Claude is NOT
* matched here (different transport, no conflict).
*
* Being excluded here no longer means the schema is LOST for provider
* "anthropic": the client puts the JSON Schema on
* `providerOptions.anthropic.finalResultSchema` and its `doGenerate` appends
* an additive `final_result` tool (see providers/anthropic/structuredOutput.ts)
* — schema enforcement without giving up tool calling. "bedrock" has no such
* handling (it talks to the raw AWS SDK directly) and still falls back to
* text-mode coercion.
*/
export function isNativeAnthropicProvider(providerName) {
return providerName === "anthropic" || providerName === "bedrock";
}
/**
* True when structured output must be disabled for this call because tools are
* active on a provider that cannot combine them (Gemini, or the native Anthropic
* Messages API surface). Mirrors the AI-SDK constraint exactly.
*/
export function isToolsSchemaExclusionInForce(providerName, modelName, shouldUseTools, toolCount) {
return ((isGeminiProvider(providerName, modelName) ||
isNativeAnthropicProvider(providerName)) &&
shouldUseTools &&
toolCount > 0);
}
/**
* True when a provider error indicates the request was rejected because JSON /
* structured output and tool-calling cannot be combined for that provider
* (e.g. Groq: "json mode cannot be combined with tool/function calling").
*
* This is the runtime, provider-agnostic complement to the static Gemini gate:
* any provider with the same limitation can be detected from its error message
* and retried without structured output instead of failing the call.
*/
export function isToolsSchemaConflictError(error) {
const message = error instanceof Error
? error.message
: typeof error === "string"
? error
: "";
return (/cannot be combined with (a )?(tool|function)/i.test(message) ||
/json[\s_-]?(mode|schema|object|output)[^.]{0,60}(tool|function)/i.test(message) ||
/(tool|function)[\s-]?call[^.]{0,60}json[\s_-]?(mode|schema)/i.test(message) ||
/response_format[^.]{0,60}(tool|function)/i.test(message));
}
/**
* True when a provider error indicates the request was rejected because the
* JSON schema is too complex for the provider's constrained decoding. Vertex
* Gemini enforces response schemas as a state machine and rejects
* "complex" schemas (regex patterns, string min/max lengths, nested array
* caps, long enums) with a deterministic 400 INVALID_ARGUMENT:
*
* "The specified schema produces a constraint that has too many states
* for serving. Typical causes of this error are schemas with lots of
* text …, schemas with long array length limits …, or schemas using
* complex value matchers …"
*
* The error arrives wrapped (provider error formatter + upstream JSON
* envelope), so match on the distinctive substring. Retrying with the same
* schema can never succeed — the correct recovery is to drop native schema
* enforcement and coerce the text response into the schema instead
* (same flow as isToolsSchemaConflictError).
*/
export function isSchemaComplexityError(error) {
const message = error instanceof Error
? error.message
: typeof error === "string"
? error
: "";
return /too many states/i.test(message);
}
/**
* True when a provider error indicates the MODEL (not the provider as a
* whole) rejects `response_format` outright, independent of which mode was
* requested. The catalog schema declares `structuredOutput` per provider,
* not per model (`catalogCapabilitiesSchema`, `src/lib/providers/catalog/
* schema.ts` — there is no per-model override the way `tools:
* "model-dependent"` has one), so a provider can accurately claim the
* capability for its default model while a caller-selected fallback model
* rejects every `response_format` value. Novita's
* `meta-llama/llama-3.3-70b-instruct` is the recorded case: both
* `json_schema` ("does not support 'json_schema' response format.
* Supported formats: json_object") and `json_object` ("response format
* json_object is not supported") come back as 400s on that model, while
* the catalog's default model accepts both.
*
* This is intentionally a text match on the classified error, not a
* per-model catalog flag: it lets ANY vendor's per-model rejection recover
* the same way, mirroring `isToolsSchemaConflictError` /
* `isSchemaComplexityError` above. Recovery here means the same thing it
* means for those two — drop `response_format` and spell the JSON Schema
* into the prompt instead — which is exactly what CLAUDE.md's Critical
* Rule 3 requires unconditionally ("Regardless of provider,
* generate({ schema }) is guaranteed to return valid JSON in content plus
* a parsed structuredData object"): a model-specific capability gap must
* degrade to best-effort JSON, not throw.
*/
export function isResponseFormatUnsupportedError(error) {
const message = error instanceof Error
? error.message
: typeof error === "string"
? error
: "";
return (/does not support[^.]{0,80}response[\s_-]?format/i.test(message) ||
/response[\s_-]?format[^.]{0,60}(?:is )?not support/i.test(message) ||
/unsupported response[\s_-]?format/i.test(message));
}
/**
* True when a provider error indicates the request was rejected because the
* `temperature` parameter is deprecated / unsupported for the model. The newest
* Anthropic models (e.g. claude-opus-4-8, with tools + advanced beta features)
* reject `temperature` — "`temperature` is deprecated for this model." — in
* favour of reasoning-effort controls. Detect this so the call can be retried
* once without `temperature` instead of failing the turn.
*/
export function isTemperatureDeprecatedError(error) {
const message = error instanceof Error
? error.message
: typeof error === "string"
? error
: "";
return /\btemperature\b[^.]{0,40}\b(deprecated|unsupported|not[\s_-]?(supported|allowed))\b/i.test(message);
}
/**
* True when the model is known to reject the `temperature` parameter (the
* reasoning-effort Anthropic models — claude-opus-4-8 and newer — deprecate it
* in favour of effort controls). Used to omit `temperature` proactively so the
* request does not fail-then-retry on every turn: the reactive
* isTemperatureDeprecatedError() retry remains the safety net for any model not
* matched here, but a guaranteed-to-fail first request is pure wasted latency.
*
* Matches opus 4.8+ (4-8, 4-9, 4-10, …) while leaving 4.1/4.5/4.6 and Sonnet/
* Haiku — which still accept `temperature` — untouched.
*/
export function modelDeprecatesTemperature(modelName) {
const m = (modelName ?? "").toLowerCase();
return /opus[-_.]?4[-_.]?(?:[89]|\d{2,})\b/.test(m);
}