Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@
"test:auth": "npx tsx test/continuous-test-suite-auth.ts",
"test:autoresearch": "npx tsx test/continuous-test-suite-autoresearch.ts",
"test:autoresearch:redis": "npx tsx test/continuous-test-suite-autoresearch-redis.ts",
"test:anthropic-tools-policy": "npx tsx test/continuous-test-suite-anthropic-tools-policy.ts",
"test:anthropic-multimodal": "npx tsx test/continuous-test-suite-anthropic-multimodal.ts",
"test:excel-interop": "npx tsx test/continuous-test-suite-excel-interop.ts",
"test:envguard": "npx tsx test/helpers/envGuard.test.ts",
"test:tool-routing-cli:vitest": "pnpm exec vitest run test/toolRoutingCli.test.ts",
"test:tool-routing-cli": "pnpm run test:tool-routing-cli:vitest && npx tsx test/continuous-test-suite-tool-routing-cli.ts",
Expand All @@ -131,7 +134,7 @@
"test:tool-routing": "pnpm run test:unit:vitest && npx tsx test/continuous-test-suite-tool-routing.ts",
"test:tool-routing-semantic:vitest": "pnpm exec vitest run test/toolRoutingSemantic.test.ts",
"test:tool-routing-semantic": "pnpm run test:tool-routing-semantic:vitest && npx tsx test/continuous-test-suite-tool-routing-semantic.ts",
"test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:unit:vitest && pnpm run test:tool-routing-cli:vitest && pnpm run test:tool-dedup:vitest && pnpm run test:model-pool:vitest && pnpm run test:tool-routing-semantic:vitest",
"test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:unit:vitest && pnpm run test:tool-routing-cli:vitest && pnpm run test:tool-dedup:vitest && pnpm run test:model-pool:vitest && pnpm run test:tool-routing-semantic:vitest && pnpm run test:anthropic-tools-policy && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop",
"// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit)": "",
"test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
"// CI tier — product output (image/video/TTS/PPT) — costs $$ per run": "",
Expand Down Expand Up @@ -598,6 +601,9 @@
"rollup@>=4.0.0 <4.59.0": ">=4.59.0",
"shell-quote@<1.8.4": ">=1.8.4",
"undici@>=8.0.0": ">=7.24.0 <8.0.0"
},
"patchedDependencies": {
"mammoth@1.12.0": "patches/mammoth@1.12.0.patch"
}
},
"os": [
Expand Down
26 changes: 26 additions & 0 deletions patches/mammoth@1.12.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
diff --git a/lib/xml/xmldom.js b/lib/xml/xmldom.js
index 752c353f..45cc659a 100644
--- a/lib/xml/xmldom.js
+++ b/lib/xml/xmldom.js
@@ -4,13 +4,18 @@ var dom = require("@xmldom/xmldom/lib/dom");
function parseFromString(string) {
var error = null;

+ // @xmldom/xmldom >= 0.9 replaced the `errorHandler` constructor option with
+ // `onError` and made the `mimeType` argument to parseFromString required.
+ // Capture only real (non-warning) errors so benign warnings don't abort.
var domParser = new xmldom.DOMParser({
- errorHandler: function(level, message) {
- error = {level: level, message: message};
+ onError: function(level, message) {
+ if (level === "error" || level === "fatalError") {
+ error = {level: level, message: message};
+ }
}
});

- var document = domParser.parseFromString(string);
+ var document = domParser.parseFromString(string, "text/xml");

if (error === null) {
return document;
11 changes: 8 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions src/lib/core/modules/GenerationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
} from "../../utils/tokenUtils.js";
import { DEFAULT_MAX_STEPS } from "../constants.js";
import {
isTemperatureDeprecatedError,
isToolsSchemaConflictError,
isToolsSchemaExclusionInForce,
} from "./structuredOutputPolicy.js";
Expand Down Expand Up @@ -266,7 +267,7 @@
/**
* Execute the generation with AI SDK
*/
async executeGeneration(

Check warning on line 270 in src/lib/core/modules/GenerationHandler.ts

View workflow job for this annotation

GitHub Actions / 🛡️ Code Quality & Security Gate

Async method 'executeGeneration' has too many lines (352). Maximum allowed is 300

Check warning on line 270 in src/lib/core/modules/GenerationHandler.ts

View workflow job for this annotation

GitHub Actions / test (20)

Async method 'executeGeneration' has too many lines (352). Maximum allowed is 300

Check warning on line 270 in src/lib/core/modules/GenerationHandler.ts

View workflow job for this annotation

GitHub Actions / test (20)

Async method 'executeGeneration' has too many lines (352). Maximum allowed is 300
model: LanguageModel,
messages: ModelMessage[],
tools: Record<string, Tool>,
Expand All @@ -275,7 +276,7 @@
return genTracer.startActiveSpan(
"neurolink.executeGeneration",
{ kind: SpanKind.INTERNAL },
async (span) => {

Check warning on line 279 in src/lib/core/modules/GenerationHandler.ts

View workflow job for this annotation

GitHub Actions / 🛡️ Code Quality & Security Gate

Async arrow function has too many lines (341). Maximum allowed is 300

Check warning on line 279 in src/lib/core/modules/GenerationHandler.ts

View workflow job for this annotation

GitHub Actions / test (20)

Async arrow function has too many lines (341). Maximum allowed is 300

Check warning on line 279 in src/lib/core/modules/GenerationHandler.ts

View workflow job for this annotation

GitHub Actions / test (20)

Async arrow function has too many lines (341). Maximum allowed is 300
const shouldUseTools = !options.disableTools && this.supportsToolsFn();
const toolCount = Object.keys(tools || {}).length;

Expand Down Expand Up @@ -531,6 +532,81 @@
return result;
}

// Retry once without `temperature` when the model deprecated it. 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. Structured
// output is already excluded for the native anthropic surface, so this
// is the dominant failure mode for Opus there.
if (
isTemperatureDeprecatedError(error) &&
typeof options.temperature === "number"
) {
span.setAttribute("neurolink.has_fallback", true);
span.addEvent("retry.initial_failure", {
"error.message":
error instanceof Error ? error.message : String(error),
"retry.attempt": 1,
"retry.reason": "temperature_deprecated",
});
logger.debug(
"[GenerationHandler] temperature-deprecated error caught - retrying without temperature",
{
provider: this.providerName,
model: this.modelName,
error: error instanceof Error ? error.message : String(error),
},
);
const result = await withProviderRetry(
() =>
this.callGenerateText(
model,
messages,
tools,
{ ...options, temperature: undefined },
shouldUseTools,
true, // mirror the initial call; the structured-output policy still applies
),
span,
"generateText(no-temperature)",
);
span.addEvent("retry.recovered", {
"retry.attempts": 2,
"retry.strategy": "temperature_omitted",
});
span.setAttribute("retry.count", 1);
if (result.usage) {
span.setAttribute(
"gen_ai.usage.input_tokens",
result.usage.inputTokens || 0,
);
span.setAttribute(
"gen_ai.usage.output_tokens",
result.usage.outputTokens || 0,
);
const noTempCost = calculateCost(
this.providerName,
this.modelName,
{
input: result.usage.inputTokens || 0,
output: result.usage.outputTokens || 0,
total:
(result.usage.inputTokens || 0) +
(result.usage.outputTokens || 0),
},
);
span.setAttribute("neurolink.cost", noTempCost ?? 0);
}
if (result.finishReason) {
span.setAttribute(
"gen_ai.response.finish_reason",
result.finishReason,
);
}
span.setStatus({ code: SpanStatusCode.OK });
return result;
}

span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : String(error),
Expand Down
73 changes: 66 additions & 7 deletions src/lib/core/modules/structuredOutputPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
* disabled because the provider cannot combine tool calls with JSON-schema
* enforcement.
*
* This is a GEMINI-ONLY API limitation. Anthropic Claude — including when
* hosted on Vertex (modelName starts with "claude-") — supports tools and
* structured output simultaneously, so it must NOT be excluded. A gate keyed on
* "any Vertex model" wrongly disables structured output for Vertex+Claude (the
* primary production config) and forces fragile hand-parsed JSON.
* 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). */
Expand All @@ -26,9 +32,21 @@ export function isGeminiProvider(
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).
*/
export function isNativeAnthropicProvider(providerName: string): boolean {
return providerName === "anthropic" || providerName === "bedrock";
}

/**
* True when structured output must be disabled for this call because tools are
* active on a Gemini provider. Mirrors the AI-SDK constraint exactly.
* 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: string,
Expand All @@ -37,7 +55,10 @@ export function isToolsSchemaExclusionInForce(
toolCount: number,
): boolean {
return (
isGeminiProvider(providerName, modelName) && shouldUseTools && toolCount > 0
(isGeminiProvider(providerName, modelName) ||
isNativeAnthropicProvider(providerName)) &&
shouldUseTools &&
toolCount > 0
);
}

Expand Down Expand Up @@ -68,3 +89,41 @@ export function isToolsSchemaConflictError(error: unknown): boolean {
/response_format[^.]{0,60}(tool|function)/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: unknown): boolean {
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: string | undefined,
): boolean {
const m = (modelName ?? "").toLowerCase();
return /opus[-_.]?4[-_.]?(?:[89]|\d{2,})\b/.test(m);
}
12 changes: 11 additions & 1 deletion src/lib/processors/document/ExcelProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,17 @@ async function loadExcelJS() {
return _exceljs;
}
try {
_exceljs = await import(/* @vite-ignore */ "exceljs");
const mod: unknown = await import(/* @vite-ignore */ "exceljs");
// exceljs is a CommonJS module. Under Node ESM (and some bundlers) the
// `Workbook` constructor is exposed at runtime on the namespace's `default`
// export rather than on the namespace itself — so a bare
// `new ExcelJS.Workbook()` throws "ExcelJS.Workbook is not a constructor"
// (TS still types it as present via esModuleInterop, masking the bug).
// Normalise here so the constructor is reachable regardless of interop style.
const ns = mod as { Workbook?: unknown; default?: unknown };
_exceljs = (
ns.Workbook ? ns : (ns.default ?? ns)
) as typeof import("exceljs");
return _exceljs;
} catch (err) {
const e = err instanceof Error ? (err as NodeJS.ErrnoException) : null;
Expand Down
Loading
Loading