Skip to content

fix(proxy): address missed CodeRabbit outside-diff comments from PR #918 - #919

Merged
murdore merged 1 commit into
releasefrom
refactor/decompose-proxy-routes-neurolink-core
Apr 2, 2026
Merged

murdore merged 1 commit into
releasefrom
refactor/decompose-proxy-routes-neurolink-core

Conversation

@murdore

@murdore murdore commented Apr 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #918. Three CodeRabbit comments were posted outside the diff context (not as inline threads) and were missed in Cycle 1. This PR resolves them along with two nitpicks.

  • requestLogger.ts L642–654 — OTLP body log silently dropped when writeBodyArtifact throws; populated stored from preparedBody in the catch block so OTLP logging proceeds via memory fallback
  • mcp.ts L3033–3046 — findToolForAnnotation() silently picked first match in multi-server ambiguity; now returns "ambiguous" and caller exits with a clear --server <id> message
  • scorerRegistry.ts L467–479 — try-finally without catch left initPromise permanently rejected on failure; added catch that resets initPromise = null before rethrowing to allow retry
  • **redisTaskStore.ts L257** — expire()` failure was only warn-logged; added 3-attempt retry with 100ms/200ms backoff before warn log
  • factory.ts (nitpick) — withTimeout calls used raw new Error; replaced with ErrorFactory.toolTimeout(...) for consistent typed errors
  • scripts/security-check.ts (nitpick) — added #xxx issue-tracking placeholder to lodash ignore entries

Test plan

  • TypeScript: ✅ 0 errors, 0 warnings
  • Lint: ✅ Prettier + ESLint clean
  • Build: ✅ Full SDK + CLI

Summary by CodeRabbit

Release Notes

  • Documentation

    • Updated documentation links and routing structure across tutorials and references.
    • Added prerequisite installation steps for @juspay/neurolink CLI in setup guides.
    • Removed outdated redirect mapping.
  • New Features

    • Improved proxy startup flow with better initialization phases and error handling.
    • Enhanced MCP command structure with better tool resolution and ambiguity detection.
  • Bug Fixes

    • Fixed environment variable handling in observability setup scripts.
    • Improved error handling and validation across AI provider integrations.
    • Better UTF-8 byte tracking for request/response logging.
  • Chores

    • Updated OpenTelemetry dependencies to latest versions.
    • Added security dependency overrides and vulnerability mitigation.

Copilot AI review requested due to automatic review settings April 2, 2026 05:40
@vercel

vercel Bot commented Apr 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neurolink Ready Ready Preview, Comment Apr 2, 2026 7:39am

@coderabbitai

coderabbitai Bot commented Apr 2, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d337a70b-5ebe-42ee-862d-461d94531f7e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This large release PR refactors core generation logic in BaseProvider with new helper methods, reorganizes proxy CLI startup into structured phases with enhanced guards, improves error handling and resilience across task management and database operations, enhances OpenTelemetry instrumentation, updates documentation routing and metadata, adds UTF-8 byte-aware truncation throughout request/response handling, and updates multiple AI provider implementations for improved type safety and state management.

Changes

Cohort / File(s) Summary
Documentation & Routing
docs-site/config/redirects.ts, docs/business-documentation.md, docs/tutorials.md
Removed legacy analytics→observability redirect; added YAML front matter (slug: tutorials/step-by-step) to tutorials; updated business doc link to use new route structure.
Documentation Content
docs/cookbook/rate-limit-handling.md, docs/features/claude-proxy.md, docs/features/claude-proxy-observability.md, docs/reference/provider-selection.md
Updated Redis method calls to camelCase typed equivalents with destructuring; added @juspay/neurolink CLI installation prerequisites; removed "(no rate limits)" parenthetical from provider selection comment.
Build & Script Enhancement
docs-site/scripts/build-llms-txt.ts
Introduced dynamic truncation tuning constants and iterative summary rebuilding via buildSummaryWithinTarget() to meet size targets within UTF-8 byte constraints.
Observability Scripts
scripts/observability/check-proxy-telemetry.mjs, scripts/observability/manage-local-openobserve.sh
Made proxy log file reading non-fatal on missing files; refactored environment variable parsing to manual line-by-line processing with validation, skipping comments/blanks; masked password output in setup feedback.
Security & Dependencies
scripts/security-check.ts, package.json
Added lodash and lodash-es to ignored vulnerabilities; removed unused allIgnored computation; updated OpenTelemetry OTLP exporter to ^0.214.0; added @xmldom/xmldom override constraint.
MCP CLI Refactoring
src/cli/commands/mcp.ts
Extracted annotation and tool-lookup logic into static helpers (listToolAnnotations, findToolForAnnotation, buildAnnotationsFromArgs, printAnnotationUpdate); added MCP status timeout wrapper with 30-second default; improved ambiguous tool detection with explicit --server guidance.
Proxy CLI Refactoring
src/cli/commands/proxy.ts
Decomposed startup into structured phases: ensureProxyStartAllowed (guards), loadProxyStartEnv, loadProxyStartConfiguration, createProxyStartApp, initializeProxyOpenTelemetry, background maintenance, and centralized startProxyRuntime; updated status output to include mode field and proxyInstallCommand path resolution; added launchd/port-conflict safety checks.
Core Provider Generation
src/lib/core/baseProvider.ts
Refactored generate() into runGenerateInActiveContext() helper executing within OTel context; extracted TTS, video-frame, and standard-flow logic into handleDirectTTSSynthesis, handleVideoFrameGeneration, executeStandardGenerateFlow, synthesizeAIResponseIfNeeded; added public gen() alias method; improved span lifecycle coordination via shared mutable state object.
Provider Factory
src/lib/core/factory.ts
Centralized model resolution into resolveModelFromEnvironment() and resolveDynamicModelName(); extracted provider registration/instantiation into createResolvedProvider() with unified timeout error handling via ErrorFactory.toolTimeout().
AI Provider: Google
src/lib/providers/googleAiStudio.ts
Removed startup-time environment variable fallback that copied GOOGLE_AI_API_KEY to GOOGLE_GENERATIVE_AI_API_KEY.
AI Provider: Google Vertex
src/lib/providers/googleVertex.ts
Refactored Gemini 3 tool-calling with dedicated maybeExecuteNativeGemini3ToolStream path; extracted helpers for tool resolution, option building, stream observation, and native loop execution (runNativeGemini3StreamLoop); centralized OTel span wrapping.
AI Provider: OpenAI
src/lib/providers/openAI.ts
Pre-computed resolvedToolChoice before request; added guard for filtered tool detection with fallback to "auto"; extracted stream transformation into createOpenAITransformedStream and chunk extraction into extractOpenAIChunkContent helpers.
AI Provider: LiteLLM
src/lib/providers/litellm.ts
Extracted stream transformation into createLiteLLMTransformedStream() async generator; improved error formatting with multi-line arguments; applied formatProviderError to error chunks; tightened fallback-model parsing to filter empty strings.
AI Provider: Ollama
src/lib/providers/ollama.ts
Introduced typed OllamaHttpError with structured fields (statusCode, statusText, responseBody); added isOllamaHttpError type guard; updated 404-handling logic to prefer response body over message parsing.
Proxy OAuth & Request Handling
src/lib/proxy/oauthFetch.ts, src/lib/proxy/claudeFormat.ts
Refactored OAuth fetch into helpers for URL resolution, header merging, auth application, body transformation, and streaming rewrite; improved request/response serialization formatting; adjusted thinking-config and tool-choice normalization logic.
Proxy Configuration & Network
src/lib/proxy/proxyConfig.ts, src/lib/proxy/proxyFetch.ts
Added array guard to loadProxyConfig accounts extraction; refactored proxy fetch into direct vs. proxied handlers with request cloning for retry; replaced proxy-agent cache keying with SHA256 hash; introduced mergeTraceHeaders helper.
Proxy Body & Stream Handling
src/lib/proxy/requestLogger.ts, src/lib/proxy/sseInterceptor.ts
Added UTF-8 byte–based redaction and chunking for captured bodies; introduced bodyTruncated metadata flag; refactored cleanup to recursively collect managed files and prune empty directories; updated byte-capping to use UTF-8 truncation utilities.
Task Management & Persistence
src/lib/tasks/backends/bullmqBackend.ts, src/lib/tasks/store/redisTaskStore.ts, src/lib/tasks/taskManager.ts
Enhanced BullMQBackend.schedule() with executor cleanup on failure; refactored RedisTaskStore TTL application with async retry logic and client reuse; added comprehensive error recovery to TaskManager (schedule rollback, history clearing guards, state restoration helpers).
Evaluation & MCP Tools
src/lib/evaluation/pipeline/evaluationPipeline.ts, src/lib/evaluation/scorers/scorerRegistry.ts, src/lib/mcp/toolRegistry.ts
Replaced generic Error with ErrorFactory.invalidConfiguration() in validation; refactored scorer registration into definition arrays with bulk registration helpers; extracted tool resolution and execution-context creation into resolveToolExecutionTarget and createExecutionContext.
Auth & Caching
src/lib/auth/anthropicOAuth.ts
Introduced bounded in-memory cache for Claude Code identities (max 1024 entries) with enforceClaudeCodeIdentityCacheLimit() purging expired/oldest entries.
Telemetry & Observability
src/lib/services/server/ai/observability/instrumentation.ts, src/lib/telemetry/telemetryService.ts
Extracted createOtelResource, initializeOtlpMetricsAndLogs, createLangfuseProcessor helpers; reorganized into initializeExternalOpenTelemetryMode and initializeStandaloneOpenTelemetryMode; improved external TracerProvider detection via constructor.name and optional getDelegate() method.
Provider Health & Utilities
src/lib/utils/providerHealth.ts, src/lib/utils/providerUtils.ts
Propagated timeout through health checks; added getConnectivityHeaders() helper; refined model-name matching for @-containing models; replaced direct Ollama/LiteLLM availability checks with ProviderHealthChecker.checkFallbackProviderAvailability().
Type Definitions
src/lib/types/proxyTypes.ts, src/lib/types/streamTypes.ts
Tightened ParsedClaudeRequest.inputSchema to required; added ProxyMode union; introduced extensive new proxy runtime/logging types (ProxyBodyCaptureLogger, AnthropicLoopState, AnthropicUpstreamFetchResult, etc.) with OTel Span references; reformatted type signatures for clarity.
Testing
test/continuous-test-suite.ts
Updated Claude Code session identity validation to parse and verify CLAUDE CODE format via parseClaudeCodeUserId instead of simple prefix checking.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • #918: Overlapping refactors of identical logic across the same files (proxy CLI, baseProvider, proxy utilities, scorer registry).
  • #915: Modifies proxy observability, telemetry surfaces, and overlapping proxy-related modules.
  • #147: Refactors baseProvider.ts generation flow and extraction of helper methods.

Suggested labels

released


🐰 Hop along, the refactors flow so neat,
From generation helpers to observability sweet,
UTF-8 bytes truncate with care so fine,
Proxy phases structured, now all in line—
What a grand release to make us all shine! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main purpose of the PR: addressing missed CodeRabbit review comments from a prior pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/decompose-proxy-routes-neurolink-core

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Follow-up changes that address previously missed review feedback from PR #918, primarily improving proxy reliability/observability and tightening error handling around provider health checks, MCP tooling, task scheduling, and security tooling.

Changes:

  • Hardened proxy observability/logging and fetch behavior (UTF-8 safe truncation, OTLP/log export setup refactors, improved fallback behavior when body artifact persistence fails).
  • Improved robustness of MCP and task infrastructure (clearer tool/server disambiguation, safer task schedule rollback/cleanup, Redis TTL retry).
  • Refactors/cleanup across provider selection/health checks, typed errors/timeouts, dependency overrides, and documentation site adjustments.

Reviewed changes

Copilot reviewed 41 out of 44 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/continuous-test-suite.ts Updates cloaking runtime test to validate Claude Code-style session identity formatting.
src/lib/utils/providerUtils.ts Uses ProviderHealthChecker for LiteLLM/Ollama availability and updates Ollama env handling.
src/lib/utils/providerHealth.ts Propagates timeout into config checks; adds connectivity headers; adjusts model matching; refactors timeout usage.
src/lib/types/streamTypes.ts Formatting-only changes to multi-imports and long union type formatting.
src/lib/types/proxyTypes.ts Adds proxy mode/body capture related types and makes tool inputSchema required in ParsedClaudeRequest tools map type.
src/lib/telemetry/telemetryService.ts Improves detection of externally-registered TracerProvider delegates.
src/lib/tasks/taskManager.ts Adds better cleanup/rollback on scheduling failures; defers history clear with error handling.
src/lib/tasks/store/redisTaskStore.ts Passes Redis client to TTL setter and adds retry/backoff for expire() calls.
src/lib/tasks/backends/bullmqBackend.ts Ensures executor cache cleanup when schedule fails; minor logging fix.
src/lib/services/server/ai/observability/instrumentation.ts Refactors OTEL/Langfuse initialization into helpers; adds OTLP metrics/logs initialization helper.
src/lib/proxy/sseInterceptor.ts Makes truncation byte-accurate for UTF-8; updates accumulator byte counting.
src/lib/proxy/requestLogger.ts Adds capped body capture, UTF-8 chunking for OTLP, and in-memory fallback when artifact persistence fails; improves cleanup to include body artifacts.
src/lib/proxy/proxyFetch.ts Refactors header merging/trace injection; introduces proxied/direct handlers and request cloning for fallback safety.
src/lib/proxy/proxyConfig.ts Tightens raw accounts object validation (rejects arrays).
src/lib/proxy/oauthFetch.ts Refactors OAuth fetch into composable helpers; routes through createProxyFetch; rewrites MCP-prefixed streaming responses.
src/lib/proxy/claudeFormat.ts Formatting-only changes; no behavioral changes intended.
src/lib/providers/openAI.ts Resolves toolChoice once and guards against toolChoice referencing filtered-out tools; extracts stream transform helpers.
src/lib/providers/ollama.ts Returns typed ProviderError for HTTP errors; improves 404 detection based on status/body.
src/lib/providers/litellm.ts Formatting and stream transform refactor; minor error formatting improvements.
src/lib/providers/googleAiStudio.ts Removes env-var aliasing block for GOOGLE_* API key.
src/lib/mcp/toolRegistry.ts Extracts tool resolution/execution context creation; uses ErrorFactory for ambiguity cases.
src/lib/evaluation/scorers/scorerRegistry.ts Moves built-in scorer definitions into tables; fixes initPromise reset on failure for retry.
src/lib/evaluation/pipeline/evaluationPipeline.ts Uses ErrorFactory for invalid options configuration error.
src/lib/core/factory.ts Uses typed timeout errors; refactors model resolution and provider creation into helpers.
src/lib/core/baseProvider.ts Refactors generate() control flow into helper methods; maintains gen() alias; improves span-ending control.
src/lib/auth/anthropicOAuth.ts Adds bounded cache eviction for Claude Code identity cache.
src/cli/commands/mcp.ts Adds MCP status timeout; refactors annotate/list flows; improves ambiguous tool handling messaging.
scripts/security-check.ts Adds lodash ignore placeholders; changes ignored vulnerability handling logic.
scripts/observability/manage-local-openobserve.sh Replaces source-based env loading with safe line-by-line parsing; avoids printing password directly.
scripts/observability/check-proxy-telemetry.mjs Treats missing summary file as retryable within loop (continue).
pnpm-lock.yaml Adds override for xmldom and bumps OTLP trace exporter version.
package.json Bumps @opentelemetry/exporter-trace-otlp-http and adds xmldom override.
docs/tutorials.md Adds frontmatter (title/slug) for docs site routing.
docs/reference/provider-selection.md Comment wording update about provider priority rationale.
docs/features/claude-proxy.md Adds CLI install step before proxy quick start.
docs/features/claude-proxy-observability.md Adds CLI install step before observability setup.
docs/cookbook/rate-limit-handling.md Updates Redis v4 method names and zset call patterns.
docs/business-documentation.md Updates quick start link to new tutorials slug route.
docs-site/scripts/build-llms-txt.ts Iteratively reduces truncation to keep llms.txt summary under size target.
docs-site/config/redirects.ts Removes one analytics redirect entry.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

scripts/security-check.ts:166

  • The ignored-package check currently passes the audit as soon as any ignored package appears in the output. This can mask additional vulnerabilities in non-ignored packages (e.g., output contains both lodash and another vulnerable dependency) and incorrectly sets the scan to "passed".

Consider parsing the audit output to extract the set of vulnerable package names and only treating the scan as passed when that set is a subset of IGNORED_VULNERABLE_PACKAGES; otherwise continue with severity counting / failure logic.

        // Check if vulnerabilities are from ignored packages
        const isIgnoredPackage = IGNORED_VULNERABLE_PACKAGES.some(
          (pkg) =>
            output.includes(`│ Package             │ ${pkg}`) ||
            output.includes(`Package: ${pkg}`),
        );

        if (isIgnoredPackage) {
          const ignoredList = IGNORED_VULNERABLE_PACKAGES.join(", ");
          this.log(
            `Found vulnerabilities in temporarily ignored packages: ${ignoredList}`,
            "cyan",
          );
          this.log(
            "No critical vulnerabilities (ignored packages excluded)",
            "green",
          );
          this.results.dependencies.status = "passed";
          return;
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1014 to 1027
private static hasRequestedModel(
availableModels: string[],
requestedModel: string,
): boolean {
const normalizedRequestedModel = requestedModel.trim();
const requiresExactMatch = /@/.test(normalizedRequestedModel);

return availableModels.some(
(model) =>
model === requestedModel ||
model.startsWith(`${requestedModel}:`) ||
requestedModel.startsWith(`${model}:`),
model === normalizedRequestedModel ||
(!requiresExactMatch &&
(model.startsWith(`${normalizedRequestedModel}:`) ||
model.startsWith(`${normalizedRequestedModel}@`))),
);

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasRequestedModel() no longer matches common Ollama tag variants (e.g. configured gemma3:27b vs installed gemma3:27b-fp16), because it only allows model.startsWith(<requested> + ':') or ... + '@'. This can cause false negatives in Ollama/LiteLLM availability checks and prevent selecting an otherwise working local fallback.

Consider restoring the previous bidirectional/prefix matching (or explicitly allowing - suffix variants) so requestedModel can match installed variants like *-fp16.

Copilot uses AI. Check for mistakes.
Comment on lines +559 to +564
const cacheKey = createHash("sha256")
.update(maskProxyUrl(proxyUrl) ?? proxyUrl)
.digest("hex");
const dispatcher =
agentCache.get(cacheKey) || (await createProxyAgent(proxyUrl));
agentCache.set(cacheKey, dispatcher);

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ProxyAgent cache key is derived from maskProxyUrl(proxyUrl), which replaces username/password with ***. If HTTP_PROXY and HTTPS_PROXY (or other proxy envs) differ only by credentials, they will collide to the same cache key and may reuse a ProxyAgent configured with the wrong credentials.

To avoid collisions without storing plaintext credentials, consider hashing the original proxyUrl (or a canonical form that retains credential uniqueness) and only using maskProxyUrl for logging.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/lib/evaluation/scorers/scorerRegistry.ts (1)

467-482: ⚠️ Potential issue | 🟠 Major

The rejected promise blocks retry indefinitely.

When ScorerRegistry.registerBuiltInLLMScorers() or ScorerRegistry.registerBuiltInRuleScorers() throws, the async IIFE immediately rejects. The synchronous assignment ScorerRegistry.initPromise = (async () => { ... })() completes before the async body executes, so the rejected promise is already stored. The catch block then sets initPromise = null, but the rejected promise object remains in the variable assignment—the next caller still receives the same permanently rejected initializer.

The proposed fix is correct: separate promise creation from assignment, await the promise after assignment, and check if (ScorerRegistry.initPromise === initPromise) before resetting to avoid races with concurrent retries. Also reset ScorerRegistry.initialized = false on error so the state machine is correct.

🐛 Proposed fix
-    ScorerRegistry.initPromise = (async () => {
-      try {
-        ScorerRegistry.registerBuiltInLLMScorers();
-        ScorerRegistry.registerBuiltInRuleScorers();
-
-        ScorerRegistry.initialized = true;
-        logger.debug(
-          `Registered ${ScorerRegistry.scorers.size} built-in scorers (including aliases)`,
-        );
-      } catch (err) {
-        ScorerRegistry.initPromise = null; // allow retry on next call
-        throw err;
-      }
-    })();
-
-    return ScorerRegistry.initPromise;
+    const initPromise = (async () => {
+      ScorerRegistry.registerBuiltInLLMScorers();
+      ScorerRegistry.registerBuiltInRuleScorers();
+
+      ScorerRegistry.initialized = true;
+      logger.debug(
+        `Registered ${ScorerRegistry.scorers.size} built-in scorers (including aliases)`,
+      );
+    })();
+
+    ScorerRegistry.initPromise = initPromise;
+
+    try {
+      await initPromise;
+    } catch (err) {
+      if (ScorerRegistry.initPromise === initPromise) {
+        ScorerRegistry.initPromise = null; // allow retry on next call
+      }
+      ScorerRegistry.initialized = false;
+      throw err;
+    }
+
+    return initPromise;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/evaluation/scorers/scorerRegistry.ts` around lines 467 - 482, The
current async IIFE assigned to ScorerRegistry.initPromise stores a rejected
promise on error and blocks retries; fix by creating a local promise (e.g.,
const initPromise = (async () => { ... })()), assign it to
ScorerRegistry.initPromise before awaiting it, then await initPromise; in the
catch handler check if (ScorerRegistry.initPromise === initPromise) before
resetting ScorerRegistry.initPromise = null to avoid races, and also set
ScorerRegistry.initialized = false on error; keep registerBuiltInLLMScorers and
registerBuiltInRuleScorers calls inside the local async, and only set
initialized = true after successful completion.
src/lib/services/server/ai/observability/instrumentation.ts (1)

626-637: ⚠️ Potential issue | 🟠 Major

Allow external-mode initialization to retry after an abort.

Both branches set isInitialized = true even though no processor/provider was created. Once that flips, later initializeOpenTelemetry() calls only update config and never retry setup, so one missing-credentials startup or transient processor-construction failure can permanently disable observability for the rest of the process.

Also applies to: 716-725

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/services/server/ai/observability/instrumentation.ts` around lines 626
- 637, The abort path in initializeOpenTelemetry() currently flips isInitialized
= true when langfuseRequested && !hasLangfuseCreds (and the similar branch at
the other location), preventing future retries; change these abort branches (the
blocks that log the warning about missing credentials/OTLP endpoint) to set
isCredentialsValid = false but do NOT set isInitialized = true so the next
initializeOpenTelemetry() call can attempt setup again; keep the logging and
return behavior but leave isInitialized false (and ensure no other code assumes
initialization happened when isCredentialsValid is false).
🧹 Nitpick comments (7)
src/lib/providers/openAI.ts (1)

698-700: Consider using ErrorFactory for consistent typed errors.

The error thrown here uses a raw new Error(...) instead of ErrorFactory. As per coding guidelines, typed errors via ErrorFactory should be preferred for consistency across the codebase.

♻️ Suggested refactor
-        throw new Error(
-          `OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`,
-        );
+        throw this.formatProviderError(
+          new Error(
+            `OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`,
+          ),
+        );

This would wrap the error through formatProviderError, which returns a typed ProviderError for consistency with other error paths in this provider.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/providers/openAI.ts` around lines 698 - 700, Replace the raw throw
new Error(...) with the standardized typed error path: call formatProviderError
(which uses ErrorFactory) to construct a ProviderError and throw that instead,
preserving the original message ("OpenAI streaming error with tools:
${errorMessage}. Try disabling tools with --disableTools") and attaching any
relevant context (errorMessage and provider/tool flags) so other provider error
handlers can inspect it; update the throw site in openAI.ts to throw the result
of formatProviderError(...) rather than creating a plain Error.
src/lib/providers/ollama.ts (1)

2061-2062: Consider caching the type guard result to avoid duplicate checks.

isOllamaHttpError(error) is called twice on the same object. While not a bug, this can be simplified:

♻️ Optional: Cache type guard result
-    const httpStatus = isOllamaHttpError(error) ? error.statusCode : undefined;
-    const responseBody = isOllamaHttpError(error) ? error.responseBody : "";
+    const ollamaErr = isOllamaHttpError(error) ? error : null;
+    const httpStatus = ollamaErr?.statusCode;
+    const responseBody = ollamaErr?.responseBody ?? "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/providers/ollama.ts` around lines 2061 - 2062, Duplicate calls to the
type guard isOllamaHttpError(error) should be avoided; call it once, store the
boolean (e.g., const isHttpError = isOllamaHttpError(error)) and then use that
variable to set httpStatus and responseBody (e.g., const httpStatus =
isHttpError ? error.statusCode : undefined; const responseBody = isHttpError ?
error.responseBody : "";), ensuring the variable name references
isOllamaHttpError, error, httpStatus and responseBody so the type narrowing
still applies.
src/lib/tasks/store/redisTaskStore.ts (1)

248-283: Consider extracting duplicated retry logic into a helper.

The retry loops for runsKey and histKey are nearly identical. Extracting this to a small helper would reduce duplication and make the retry parameters (attempts, backoff) easier to tune centrally.

♻️ Suggested refactor
+  private async expireWithRetry(
+    client: RedisClient,
+    key: string,
+    ttlSeconds: number,
+    taskId: string,
+    keyType: string,
+  ): Promise<void> {
+    for (let attempt = 1; attempt <= 3; attempt++) {
+      try {
+        await client.expire(key, ttlSeconds);
+        return;
+      } catch (err) {
+        if (attempt === 3) {
+          logger.warn(
+            `[TaskStore:Redis] expire failed after 3 attempts on ${keyType} key — task data may outlive retention window`,
+            { taskId, key, ttlSeconds, err: String(err) },
+          );
+        } else {
+          await new Promise((r) => setTimeout(r, 100 * attempt));
+        }
+      }
+    }
+  }

   private applyRetentionTTL(task: Task, client: RedisClient): void {
     // ... existing early-return logic ...

     const ttlSeconds = Math.ceil(ttlMs / 1000);
-    void (async () => {
-      const runsKey = taskRunsKey(task.id);
-      for (let attempt = 1; attempt <= 3; attempt++) {
-        try {
-          await client.expire(runsKey, ttlSeconds);
-          break;
-        } catch (err) {
-          if (attempt === 3) {
-            logger.warn(
-              "[TaskStore:Redis] expire failed after 3 attempts on task runs key — task data may outlive retention window",
-              { taskId: task.id, key: runsKey, ttlSeconds, err: String(err) },
-            );
-          } else {
-            await new Promise((r) => setTimeout(r, 100 * attempt));
-          }
-        }
-      }
-    })();
-    void (async () => {
-      const histKey = taskHistoryKey(task.id);
-      for (let attempt = 1; attempt <= 3; attempt++) {
-        try {
-          await client.expire(histKey, ttlSeconds);
-          break;
-        } catch (err) {
-          if (attempt === 3) {
-            logger.warn(
-              "[TaskStore:Redis] expire failed after 3 attempts on task history key — task data may outlive retention window",
-              { taskId: task.id, key: histKey, ttlSeconds, err: String(err) },
-            );
-          } else {
-            await new Promise((r) => setTimeout(r, 100 * attempt));
-          }
-        }
-      }
-    })();
+    void this.expireWithRetry(client, taskRunsKey(task.id), ttlSeconds, task.id, "task runs");
+    void this.expireWithRetry(client, taskHistoryKey(task.id), ttlSeconds, task.id, "task history");
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/tasks/store/redisTaskStore.ts` around lines 248 - 283, The duplicate
retry loops around client.expire for runsKey and histKey should be extracted
into a reusable async helper (e.g., retryExpire or withRetry) that accepts the
key, ttlSeconds, maxAttempts, and a backoff function; replace the two IIFEs that
call taskRunsKey(task.id) and taskHistoryKey(task.id) with calls to that helper,
and ensure the helper logs the same logger.warn message (including taskId, key,
ttlSeconds, and err) on the final failure and uses the same incremental backoff
(100 * attempt) between attempts.
src/cli/commands/mcp.ts (1)

3143-3145: argv.detailed isn't reachable from this command.

buildAnnotateOptions() doesn't define a --detailed flag, so this branch never runs. Either wire the option up or drop the condition to avoid dead CLI behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/mcp.ts` around lines 3143 - 3145, The branch checking
argv.detailed is dead because buildAnnotateOptions() doesn't define a --detailed
flag; either add the flag to the CLI options or remove the unreachable branch.
To fix, either (A) add a boolean detailed option in buildAnnotateOptions() so
argv.detailed becomes available (ensure parsing sets argv.detailed and update
any help/usage text), or (B) remove the if (argv.detailed) conditional and
always call logger.always(`    ${chalk.gray(tool.description)}`) in the code
where logger.always, chalk.gray and tool.description are used; choose one
approach and update tests/docs accordingly.
src/lib/core/baseProvider.ts (1)

990-1013: Reuse the shared timeout path here.

This reintroduces bespoke timeout orchestration inside generate() instead of the shared helper already on BaseProvider, so timeout semantics and typed errors can drift again.

As per coding guidelines "Use ErrorFactory for creating typed errors and wrap async operations with withTimeout utility for error handling".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/core/baseProvider.ts` around lines 990 - 1013, The generate()
implementation is reintroducing bespoke timeout orchestration
(createTimeoutController, composeAbortSignals, manual cleanup and direct await
of executeGeneration) instead of using the shared BaseProvider timeout helper;
replace the custom logic with the shared withTimeout-based wrapper (and produce
typed errors via ErrorFactory) so timeout semantics and typed errors remain
consistent: use the provider's withTimeout (or the BaseProvider helper) to wrap
the call to this.executeGeneration(model, messages, tools, options) and ensure
the wrapper passes/combines abortSignal correctly and maps timeout/rejection
errors to the provider's ErrorFactory-created typed errors (remove
createTimeoutController, composeAbortSignals, manual cleanup and direct await in
generate()).
src/cli/commands/proxy.ts (2)

827-891: Consider adding timeout protection for token refresh operations.

The refreshToken(account) calls (lines 848, 882) make external HTTP requests but have no timeout protection. If the Anthropic token endpoint becomes unresponsive, this background task could hang indefinitely, potentially blocking the refresh interval slot.

As per coding guidelines: "wrap async operations with withTimeout utility for error handling."

♻️ Suggested improvement
+import { withTimeout } from "../../lib/utils/timeout.js";
+
 async function refreshProxyTokensInBackground(): Promise<void> {
   // ...
         if (needsRefresh(account)) {
-          const result = await refreshToken(account);
+          const result = await withTimeout(
+            refreshToken(account),
+            30_000,
+            "Token refresh timed out"
+          );
           if (result.success) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/proxy.ts` around lines 827 - 891, The
refreshProxyTokensInBackground function calls refreshToken(account) twice
without timeout protection; wrap both refreshToken invocations (inside the
anthropicKeys loop and the credPath block) with the project's withTimeout
utility (e.g., await withTimeout(refreshToken(account), <reasonableMs>)) so the
background job cannot hang; ensure you import or reference withTimeout, handle
timeout/errors non-fatally the same way existing per-account try/catch does, and
only call persistTokens when the timed call returns a successful result.

923-930: Consider adding timeout protection for OpenTelemetry flush during shutdown.

The flushOpenTelemetry() and shutdownOpenTelemetry() calls have no timeout. If the OTLP endpoint is unresponsive, shutdown could hang indefinitely, preventing graceful process termination.

♻️ Suggested improvement with timeout
     try {
       const { flushOpenTelemetry, shutdownOpenTelemetry } =
         await import("../../lib/services/server/ai/observability/instrumentation.js");
-      await flushOpenTelemetry();
-      await shutdownOpenTelemetry();
+      await Promise.race([
+        (async () => {
+          await flushOpenTelemetry();
+          await shutdownOpenTelemetry();
+        })(),
+        new Promise((_, reject) =>
+          setTimeout(() => reject(new Error("OTEL shutdown timeout")), 5_000)
+        ),
+      ]).catch(() => {
+        // Timeout or error - proceed with shutdown
+      });
     } catch {
       // non-fatal — proxy shutdown must not block on OTel
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/proxy.ts` around lines 923 - 930, The shutdown path
currently awaits flushOpenTelemetry() and shutdownOpenTelemetry() with no
timeout; wrap each call (or the whole sequence) in a timeout-protected promise
(e.g., Promise.race against a short timeout) so an unresponsive OTLP endpoint
cannot hang shutdown, and on timeout log a warning/error and proceed; apply this
around the imported functions flushOpenTelemetry and shutdownOpenTelemetry so
the try/catch still prevents throws but also guarantees the process keeps
shutting down within the timeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs-site/scripts/build-llms-txt.ts`:
- Around line 656-664: The log "Adjusted truncation length to ... to stay within
target size" is printed unconditionally when summaryBuild.truncateChars differs
from SUMMARY_CONTENT_TRUNCATE_CHARS even if the final summary still exceeds the
target; change this so the adjustment message is only logged when the final size
check passes. Specifically, update the code around summaryBuild.truncateChars /
SUMMARY_CONTENT_TRUNCATE_CHARS and summaryStats.size / SUMMARY_MAX_SIZE_KB:
either move the adjusted-truncation console.log to run after the size check or
wrap it in a condition that requires summaryStats.size <= SUMMARY_MAX_SIZE_KB *
1024 (and optionally that truncateChars changed and is >=
SUMMARY_MIN_TRUNCATE_CHARS) so you don't claim "to stay within target size"
unless the final byte size actually meets the limit.

In `@scripts/security-check.ts`:
- Around line 45-46: The check that uses .some() to decide a scan "passed" when
any ignored package appears is incorrect; update the logic in
scripts/security-check.ts (the block that inspects audit results, uses
ignoredPackages and sets status = "passed") so it only returns passed when every
detected vulnerable package is contained in ignoredPackages — e.g., collect the
list of vulnerable package names from the audit output and replace the .some()
test with a subset check (or .every()) that ensures all detected vuln packages
are ignored before setting status = "passed".

In `@src/lib/core/baseProvider.ts`:
- Around line 786-820: The generate method exits early on non-standard paths
(video, image, direct TTS, and early videoFrameResult) before recording success
metrics; update provider.generate to call SpanSerializer.endSpan(...) (and any
performance recording that uses startTime) just before each early return: i.e.,
after handleVideoGeneration return path, after the image flow that calls
executeImageGeneration/enhanceResult, before returning from
handleDirectTTSSynthesis, and when returning videoFrameResult from
handleVideoFrameGeneration; ensure you pass the same span context/startTime and
include success metadata so metrics are recorded consistently for these
branches.

In `@src/lib/core/factory.ts`:
- Around line 203-206: The call to dynamicModelProvider.resolveModel is passing
the literal "default" via modelName which prevents dynamic default resolution;
update the code around dynamicModelProvider.resolveModel to normalize modelName
(or the variable normalizedProvider’s model hint) so that when modelName ===
"default" you pass undefined instead (e.g., compute a lookupModel = modelName
=== "default" ? undefined : modelName) before calling
dynamicModelProvider.resolveModel(normalizedProvider, lookupModel) so
createProvider will trigger the provider's dynamic default path.

In `@src/lib/mcp/toolRegistry.ts`:
- Line 350: The executeTool flow currently swallows the ambiguity error thrown
by resolveToolExecutionTarget (e.g., "Ambiguous tool name") and converts it into
a ToolResult failure; update executeTool (and the similar block around the other
occurrence) to detect and rethrow ambiguity lookup errors instead of returning {
success: false } — for example, check the thrown error's message or type for
"Ambiguous tool name" (in addition to the existing "not found in registry" and
"not executable" checks) and rethrow the original error so callers relying on
thrown lookup failures (resolveToolExecutionTarget) receive the correct
exception.

In `@src/lib/providers/googleVertex.ts`:
- Line 1401: The current logger.info call emits raw tool payloads
(logger.info("Tool execution completed", { toolResults, toolCalls })), which may
contain prompts, secrets, or customer data; replace this with a sanitized
summary: implement a helper (e.g., sanitizeToolPayloads or
buildToolExecutionSummary) that strips/redacts sensitive fields from toolResults
and toolCalls and returns only safe metadata (counts, tool names/ids, status
codes, durations, and coarse error messages), then call logger.info with that
summary instead of the raw objects; ensure the helper is used wherever
toolResults/toolCalls are logged so no raw payloads are emitted.
- Around line 1357-1370: The current early-return only checks analysisSchema and
misses two other structured-output signals; update the guard so it returns when
none of the structured-output indicators are present: replace the existing check
that uses only analysisSchema with a combined condition that tests
(analysisSchema || options.output?.format === "json" || options.schema) so the
code becomes: if (!(analysisSchema || options.output?.format === "json" ||
options.schema)) { return streamOptions; }—this ensures the tool-disabling block
(where streamOptions.tools, streamOptions.toolChoice, and streamOptions.stopWhen
are deleted when !isAnthropic) runs whenever any structured-output signal is
present.
- Around line 1749-1751: The span in executeNativeGemini3StreamWithSpan is being
ended before runNativeGemini3StreamLoop finishes, so subsequent
span.addEvent()/span.setAttribute() calls are lost; fix this by awaiting the
loopPromise (returned by runNativeGemini3StreamLoop) before ending the span or
by moving the span lifecycle outside withClientSpan so the span remains open
until loopPromise resolves; specifically, update
executeNativeGemini3StreamWithSpan to await loopPromise (or ensure
withClientSpan doesn't call span.end() until loopPromise resolves) and keep
references to loopPromise, runNativeGemini3StreamLoop, withClientSpan,
span.addEvent, and span.setAttribute while making this change.
- Around line 1480-1521: The three independent Promise.resolve chains for
result.usage, result.finishReason, and result.text can end the span out of
order; replace them with a coordinated Promise.all (or async/await) that awaits
all three values before calling streamSpan.end(), then set the attributes
(gen_ai.usage.*, neurolink.cost using calculateCost with
getModelId/getDefaultVertexModel and options.model, and
gen_ai.response.finish_reason) after the values are resolved; ensure any error
in the combined await sets streamSpan.setStatus({ code: SpanStatusCode.ERROR,
message: ... }) and still calls streamSpan.end() so the span always closes with
correct attributes or error status.

In `@src/lib/proxy/oauthFetch.ts`:
- Around line 178-218: When enableMcpPrefix is true, also prefix
parsed.tool_choice.name with MCP_TOOL_PREFIX so a pinned tool reference matches
the renamed tools; inside the existing enableMcpPrefix block (near parsed.tools
and parsed.messages handling) add logic to check parsed.tool_choice?.name and,
if present and not already prefixed (use startsWith(MCP_TOOL_PREFIX)), set
parsed.tool_choice.name = `${MCP_TOOL_PREFIX}${parsed.tool_choice.name}`; keep
the existing deletion of parsed.thinking for parsed.tool_choice?.type === "any"
|| "tool".

In `@src/lib/proxy/proxyFetch.ts`:
- Around line 574-593: The proxy currently reconstructs Request into
fetchInput/fetchInit (variables fetchInput, fetchInit) which strips
Request-specific semantics like duplex; instead when input is an instance of
Request pass the original Request object through to undici.fetch (i.e., call
undici.fetch(input, { dispatcher }) or merge only allowed overrides) so the
duplex flag and streaming body are preserved; update the branch that handles
input instanceof Request to avoid creating a new RequestInit with input.body and
ensure undici.fetch receives the original Request instance (or a properly
constructed Request preserving duplex) along with the dispatcher.

In `@src/lib/proxy/sseInterceptor.ts`:
- Around line 199-206: The current truncateUtf8String returns "" when maxBytes
<= 0 or less than the TRUNCATION_MARKER size, which hides the truncation marker;
instead, when maxBytes > 0 but smaller than utf8ByteLength(TRUNCATION_MARKER)
return a byte-aware truncated slice of TRUNCATION_MARKER so at least part of the
marker is visible. Update truncateUtf8String to, in the branch that checks
maxBytes and markerBytes, if maxBytes <= 0 return "" but if 0 < maxBytes <
markerBytes produce a markerPrefix by iterating characters (using utf8ByteLength
to track bytes) until maxBytes is reached and return that markerPrefix; keep the
existing behavior for larger budgets and normal truncation logic.

In `@src/lib/tasks/taskManager.ts`:
- Around line 336-351: The rollback currently calls
restoreScheduledTask(existing, ...) before rollbackTaskUpdate(taskId, existing,
error) which can leave the store inconsistent if rollbackTaskUpdate fails; swap
the two calls so you call await this.rollbackTaskUpdate(taskId, existing, error)
first, then await this.restoreScheduledTask(existing, "update schedule
rollback"); keep the TaskError.create(...) construction and the same
error/details payload (taskId, previousSchedule: existing.schedule,
attemptedSchedule) so the thrown error behavior is unchanged.
- Around line 518-536: The rollbackTaskUpdate currently throws only
rollbackError which hides the original error and prevents callers like resume()
from creating the proper TaskError (e.g., SCHEDULE_FAILED); change
rollbackTaskUpdate to preserve both errors by constructing and throwing a
wrapped error that includes the original error (error) and the rollback error
(rollbackError) as structured context (e.g., message combining both and/or
properties like originalError and rollbackError or using the cause field) so
callers can still detect and rethrow TaskError.create with schedule details;
update references in rollbackTaskUpdate and ensure resume() and other callers
can inspect the wrapped error to generate the correct TaskError.

In `@src/lib/telemetry/telemetryService.ts`:
- Around line 106-111: The delegate-branch check that computes delegateName from
provider.getDelegate()/provider._delegate only excludes "NoopTracerProvider" but
must match the top-level exclusion of both "NoopTracerProvider" and
"ProxyTracerProvider"; update the boolean return to also exclude
"ProxyTracerProvider" (i.e., require delegateName && delegateName !==
"NoopTracerProvider" && delegateName !== "ProxyTracerProvider") so a proxy
wrapper returned by getDelegate() won't be misdetected as an external provider
(referencing provider.getDelegate, provider._delegate, delegateName, and the
provider names).

---

Outside diff comments:
In `@src/lib/evaluation/scorers/scorerRegistry.ts`:
- Around line 467-482: The current async IIFE assigned to
ScorerRegistry.initPromise stores a rejected promise on error and blocks
retries; fix by creating a local promise (e.g., const initPromise = (async () =>
{ ... })()), assign it to ScorerRegistry.initPromise before awaiting it, then
await initPromise; in the catch handler check if (ScorerRegistry.initPromise ===
initPromise) before resetting ScorerRegistry.initPromise = null to avoid races,
and also set ScorerRegistry.initialized = false on error; keep
registerBuiltInLLMScorers and registerBuiltInRuleScorers calls inside the local
async, and only set initialized = true after successful completion.

In `@src/lib/services/server/ai/observability/instrumentation.ts`:
- Around line 626-637: The abort path in initializeOpenTelemetry() currently
flips isInitialized = true when langfuseRequested && !hasLangfuseCreds (and the
similar branch at the other location), preventing future retries; change these
abort branches (the blocks that log the warning about missing credentials/OTLP
endpoint) to set isCredentialsValid = false but do NOT set isInitialized = true
so the next initializeOpenTelemetry() call can attempt setup again; keep the
logging and return behavior but leave isInitialized false (and ensure no other
code assumes initialization happened when isCredentialsValid is false).

---

Nitpick comments:
In `@src/cli/commands/mcp.ts`:
- Around line 3143-3145: The branch checking argv.detailed is dead because
buildAnnotateOptions() doesn't define a --detailed flag; either add the flag to
the CLI options or remove the unreachable branch. To fix, either (A) add a
boolean detailed option in buildAnnotateOptions() so argv.detailed becomes
available (ensure parsing sets argv.detailed and update any help/usage text), or
(B) remove the if (argv.detailed) conditional and always call logger.always(`   
${chalk.gray(tool.description)}`) in the code where logger.always, chalk.gray
and tool.description are used; choose one approach and update tests/docs
accordingly.

In `@src/cli/commands/proxy.ts`:
- Around line 827-891: The refreshProxyTokensInBackground function calls
refreshToken(account) twice without timeout protection; wrap both refreshToken
invocations (inside the anthropicKeys loop and the credPath block) with the
project's withTimeout utility (e.g., await withTimeout(refreshToken(account),
<reasonableMs>)) so the background job cannot hang; ensure you import or
reference withTimeout, handle timeout/errors non-fatally the same way existing
per-account try/catch does, and only call persistTokens when the timed call
returns a successful result.
- Around line 923-930: The shutdown path currently awaits flushOpenTelemetry()
and shutdownOpenTelemetry() with no timeout; wrap each call (or the whole
sequence) in a timeout-protected promise (e.g., Promise.race against a short
timeout) so an unresponsive OTLP endpoint cannot hang shutdown, and on timeout
log a warning/error and proceed; apply this around the imported functions
flushOpenTelemetry and shutdownOpenTelemetry so the try/catch still prevents
throws but also guarantees the process keeps shutting down within the timeout.

In `@src/lib/core/baseProvider.ts`:
- Around line 990-1013: The generate() implementation is reintroducing bespoke
timeout orchestration (createTimeoutController, composeAbortSignals, manual
cleanup and direct await of executeGeneration) instead of using the shared
BaseProvider timeout helper; replace the custom logic with the shared
withTimeout-based wrapper (and produce typed errors via ErrorFactory) so timeout
semantics and typed errors remain consistent: use the provider's withTimeout (or
the BaseProvider helper) to wrap the call to this.executeGeneration(model,
messages, tools, options) and ensure the wrapper passes/combines abortSignal
correctly and maps timeout/rejection errors to the provider's
ErrorFactory-created typed errors (remove createTimeoutController,
composeAbortSignals, manual cleanup and direct await in generate()).

In `@src/lib/providers/ollama.ts`:
- Around line 2061-2062: Duplicate calls to the type guard
isOllamaHttpError(error) should be avoided; call it once, store the boolean
(e.g., const isHttpError = isOllamaHttpError(error)) and then use that variable
to set httpStatus and responseBody (e.g., const httpStatus = isHttpError ?
error.statusCode : undefined; const responseBody = isHttpError ?
error.responseBody : "";), ensuring the variable name references
isOllamaHttpError, error, httpStatus and responseBody so the type narrowing
still applies.

In `@src/lib/providers/openAI.ts`:
- Around line 698-700: Replace the raw throw new Error(...) with the
standardized typed error path: call formatProviderError (which uses
ErrorFactory) to construct a ProviderError and throw that instead, preserving
the original message ("OpenAI streaming error with tools: ${errorMessage}. Try
disabling tools with --disableTools") and attaching any relevant context
(errorMessage and provider/tool flags) so other provider error handlers can
inspect it; update the throw site in openAI.ts to throw the result of
formatProviderError(...) rather than creating a plain Error.

In `@src/lib/tasks/store/redisTaskStore.ts`:
- Around line 248-283: The duplicate retry loops around client.expire for
runsKey and histKey should be extracted into a reusable async helper (e.g.,
retryExpire or withRetry) that accepts the key, ttlSeconds, maxAttempts, and a
backoff function; replace the two IIFEs that call taskRunsKey(task.id) and
taskHistoryKey(task.id) with calls to that helper, and ensure the helper logs
the same logger.warn message (including taskId, key, ttlSeconds, and err) on the
final failure and uses the same incremental backoff (100 * attempt) between
attempts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bee57f95-96f8-41f2-b1d2-d00eaa0ea4eb

📥 Commits

Reviewing files that changed from the base of the PR and between 17f76b8 and 1098d44.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (43)
  • docs-site/config/redirects.ts
  • docs-site/scripts/build-llms-txt.ts
  • docs/business-documentation.md
  • docs/cookbook/rate-limit-handling.md
  • docs/features/claude-proxy-observability.md
  • docs/features/claude-proxy.md
  • docs/reference/provider-selection.md
  • docs/tutorials.md
  • package.json
  • scripts/observability/check-proxy-telemetry.mjs
  • scripts/observability/manage-local-openobserve.sh
  • scripts/security-check.ts
  • src/cli/commands/mcp.ts
  • src/cli/commands/proxy.ts
  • src/lib/auth/anthropicOAuth.ts
  • src/lib/core/baseProvider.ts
  • src/lib/core/factory.ts
  • src/lib/evaluation/pipeline/evaluationPipeline.ts
  • src/lib/evaluation/scorers/scorerRegistry.ts
  • src/lib/mcp/toolRegistry.ts
  • src/lib/neurolink.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/googleVertex.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/ollama.ts
  • src/lib/providers/openAI.ts
  • src/lib/proxy/claudeFormat.ts
  • src/lib/proxy/oauthFetch.ts
  • src/lib/proxy/proxyConfig.ts
  • src/lib/proxy/proxyFetch.ts
  • src/lib/proxy/requestLogger.ts
  • src/lib/proxy/sseInterceptor.ts
  • src/lib/server/routes/claudeProxyRoutes.ts
  • src/lib/services/server/ai/observability/instrumentation.ts
  • src/lib/tasks/backends/bullmqBackend.ts
  • src/lib/tasks/store/redisTaskStore.ts
  • src/lib/tasks/taskManager.ts
  • src/lib/telemetry/telemetryService.ts
  • src/lib/types/proxyTypes.ts
  • src/lib/types/streamTypes.ts
  • src/lib/utils/providerHealth.ts
  • src/lib/utils/providerUtils.ts
  • test/continuous-test-suite.ts
💤 Files with no reviewable changes (2)
  • docs-site/config/redirects.ts
  • src/lib/providers/googleAiStudio.ts

Comment on lines +656 to +664
if (summaryBuild.truncateChars !== SUMMARY_CONTENT_TRUNCATE_CHARS) {
console.log(
` Adjusted truncation length to ${summaryBuild.truncateChars} characters to stay within target size`,
);
}
if (summaryStats.size > SUMMARY_MAX_SIZE_KB * 1024) {
console.log(` Warning: Summary exceeds target size of ${SUMMARY_MAX_SIZE_KB}KB`);
console.log(
` Warning: Summary still exceeds target size of ${SUMMARY_MAX_SIZE_KB}KB`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't say the target was met before you verify the final byte size.

If the loop bottoms out at SUMMARY_MIN_TRUNCATE_CHARS and the summary is still oversized, Lines 656-659 say it was adjusted "to stay within target size" immediately before the warning on Lines 661-664. Make that log conditional on the final size check.

♻️ Suggested tweak
   const summaryStats = fs.statSync(SUMMARY_OUTPUT);
   const summarySizeKB = (summaryStats.size / 1024).toFixed(2);
+  const summaryWithinTarget = summaryStats.size <= SUMMARY_MAX_SIZE_KB * 1024;
   console.log(`  Output: ${SUMMARY_OUTPUT}`);
   console.log(`  Size: ${summarySizeKB} KB`);
   if (summaryBuild.truncateChars !== SUMMARY_CONTENT_TRUNCATE_CHARS) {
     console.log(
-      `  Adjusted truncation length to ${summaryBuild.truncateChars} characters to stay within target size`,
+      summaryWithinTarget
+        ? `  Adjusted truncation length to ${summaryBuild.truncateChars} characters to stay within target size`
+        : `  Adjusted truncation length to ${summaryBuild.truncateChars} characters while trying to reach the target size`,
     );
   }
-  if (summaryStats.size > SUMMARY_MAX_SIZE_KB * 1024) {
+  if (!summaryWithinTarget) {
     console.log(
       `  Warning: Summary still exceeds target size of ${SUMMARY_MAX_SIZE_KB}KB`,
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (summaryBuild.truncateChars !== SUMMARY_CONTENT_TRUNCATE_CHARS) {
console.log(
` Adjusted truncation length to ${summaryBuild.truncateChars} characters to stay within target size`,
);
}
if (summaryStats.size > SUMMARY_MAX_SIZE_KB * 1024) {
console.log(` Warning: Summary exceeds target size of ${SUMMARY_MAX_SIZE_KB}KB`);
console.log(
` Warning: Summary still exceeds target size of ${SUMMARY_MAX_SIZE_KB}KB`,
);
const summaryStats = fs.statSync(SUMMARY_OUTPUT);
const summarySizeKB = (summaryStats.size / 1024).toFixed(2);
const summaryWithinTarget = summaryStats.size <= SUMMARY_MAX_SIZE_KB * 1024;
console.log(` Output: ${SUMMARY_OUTPUT}`);
console.log(` Size: ${summarySizeKB} KB`);
if (summaryBuild.truncateChars !== SUMMARY_CONTENT_TRUNCATE_CHARS) {
console.log(
summaryWithinTarget
? ` Adjusted truncation length to ${summaryBuild.truncateChars} characters to stay within target size`
: ` Adjusted truncation length to ${summaryBuild.truncateChars} characters while trying to reach the target size`,
);
}
if (!summaryWithinTarget) {
console.log(
` Warning: Summary still exceeds target size of ${SUMMARY_MAX_SIZE_KB}KB`,
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs-site/scripts/build-llms-txt.ts` around lines 656 - 664, The log
"Adjusted truncation length to ... to stay within target size" is printed
unconditionally when summaryBuild.truncateChars differs from
SUMMARY_CONTENT_TRUNCATE_CHARS even if the final summary still exceeds the
target; change this so the adjustment message is only logged when the final size
check passes. Specifically, update the code around summaryBuild.truncateChars /
SUMMARY_CONTENT_TRUNCATE_CHARS and summaryStats.size / SUMMARY_MAX_SIZE_KB:
either move the adjusted-truncation console.log to run after the size check or
wrap it in a condition that requires summaryStats.size <= SUMMARY_MAX_SIZE_KB *
1024 (and optionally that truncateChars changed and is >=
SUMMARY_MIN_TRUNCATE_CHARS) so you don't claim "to stay within target size"
unless the final byte size actually meets the limit.

Comment thread scripts/security-check.ts
Comment on lines +45 to +46
"lodash", // TODO: track in #xxx — no patch available upstream for @semantic-release dev dep
"lodash-es", // TODO: track in #xxx — no patch available upstream for @semantic-release dev dep

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the pass condition is currently "any ignored package present"
rg -n -C4 'isIgnoredPackage|some\(|results\.dependencies\.status = "passed"|return;' scripts/security-check.ts

Repository: juspay/neurolink

Length of output: 2628


🏁 Script executed:

# Verify lines 45-46 show the IGNORED_VULNERABLE_PACKAGES constant
sed -n '40,55p' scripts/security-check.ts

Repository: juspay/neurolink

Length of output: 691


🏁 Script executed:

# Check full vulnerability detection and filtering logic to ensure no other guards exist
sed -n '145,210p' scripts/security-check.ts

Repository: juspay/neurolink

Length of output: 2525


Ignored-package expansion can incorrectly pass audits with real vulnerabilities

The current logic at line 148–166 uses .some() to check if ANY ignored package exists in the audit output. If true, the scan immediately returns with status = "passed" at line 165, skipping the severity check entirely. This means adding lodash and lodash-es (common packages) will cause any audit containing them to pass, even if non-ignored high/critical vulnerabilities are also present.

Suggested fix (only pass when all detected vulnerable packages are ignored)
-        const isIgnoredPackage = IGNORED_VULNERABLE_PACKAGES.some(
-          (pkg) =>
-            output.includes(`│ Package             │ ${pkg}`) ||
-            output.includes(`Package: ${pkg}`),
-        );
-
-        if (isIgnoredPackage) {
+        const detectedPackages = Array.from(
+          new Set(
+            [...output.matchAll(/Package:\s+([^\s]+)/g)].map((m) => m[1]),
+          ),
+        );
+        const allDetectedAreIgnored =
+          detectedPackages.length > 0 &&
+          detectedPackages.every((pkg) =>
+            IGNORED_VULNERABLE_PACKAGES.includes(pkg),
+          );
+
+        if (allDetectedAreIgnored) {
           const ignoredList = IGNORED_VULNERABLE_PACKAGES.join(", ");
           this.log(
             `Found vulnerabilities in temporarily ignored packages: ${ignoredList}`,
             "cyan",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/security-check.ts` around lines 45 - 46, The check that uses .some()
to decide a scan "passed" when any ignored package appears is incorrect; update
the logic in scripts/security-check.ts (the block that inspects audit results,
uses ignoredPackages and sets status = "passed") so it only returns passed when
every detected vulnerable package is contained in ignoredPackages — e.g.,
collect the list of vulnerable package names from the audit output and replace
the .some() test with a subset check (or .every()) that ensures all detected
vuln packages are ignored before setting status = "passed".

Comment on lines +786 to +820
try {
if (options.output?.mode === "video") {
return await this.handleVideoGeneration(options, startTime);
}

const isImageModel = IMAGE_GENERATION_MODELS.some((m) =>
this.modelName.includes(m),
);
if (isImageModel) {
logger.info(
`Image generation model detected, routing to executeImageGeneration`,
{
provider: this.providerName,
model: this.modelName,
},
);

if (isImageModel) {
logger.info(
`Image generation model detected, routing to executeImageGeneration`,
{
provider: this.providerName,
model: this.modelName,
},
);
const imageResult = await this.executeImageGeneration(options);
return await this.enhanceResult(imageResult, options, startTime);
}

const imageResult = await this.executeImageGeneration(options);
return await this.enhanceResult(imageResult, options, startTime);
}
if (options.tts?.enabled && !options.tts?.useAiResponse) {
return this.handleDirectTTSSynthesis(options, startTime);
}

// ===== TTS MODE 1: Direct Input Synthesis (useAiResponse=false) =====
// Synthesize input text directly without AI generation
// This is optimal for simple read-aloud scenarios
if (options.tts?.enabled && !options.tts?.useAiResponse) {
const textToSynthesize = options.prompt ?? options.input?.text ?? "";
const { tools, model } = await this.prepareGenerationContext(options);
const messages = await this.buildMessages(options);
const videoFrameResult = await this.handleVideoFrameGeneration(
options,
messages,
model,
startTime,
);
if (videoFrameResult) {
return videoFrameResult;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

End provider.generate metrics on the non-standard return paths.

The branches returning at Lines 788, 804, 808, and 820 exit before the standard flow records success metrics, so direct TTS / image / video requests now miss the success SpanSerializer.endSpan(...) and performance recording.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/core/baseProvider.ts` around lines 786 - 820, The generate method
exits early on non-standard paths (video, image, direct TTS, and early
videoFrameResult) before recording success metrics; update provider.generate to
call SpanSerializer.endSpan(...) (and any performance recording that uses
startTime) just before each early return: i.e., after handleVideoGeneration
return path, after the image flow that calls
executeImageGeneration/enhanceResult, before returning from
handleDirectTTSSynthesis, and when returning videoFrameResult from
handleVideoFrameGeneration; ensure you pass the same span context/startTime and
include success metadata so metrics are recorded consistently for these
branches.

Comment thread src/lib/core/factory.ts
Comment on lines +203 to +206
const dynamicModel = dynamicModelProvider.resolveModel(
normalizedProvider,
modelName || undefined,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize "default" before dynamic model lookup.

dynamicModelProvider.resolveModel() only picks the provider default when modelHint is undefined. Passing the literal "default" here makes it do an exact/fuzzy lookup instead, so createProvider(..., "default") skips dynamic default resolution and falls back to the static provider default.

💡 Proposed fix
       const dynamicModel = dynamicModelProvider.resolveModel(
         normalizedProvider,
-        modelName || undefined,
+        modelName && modelName !== "default" ? modelName : undefined,
       );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dynamicModel = dynamicModelProvider.resolveModel(
normalizedProvider,
modelName || undefined,
);
const dynamicModel = dynamicModelProvider.resolveModel(
normalizedProvider,
modelName && modelName !== "default" ? modelName : undefined,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/core/factory.ts` around lines 203 - 206, The call to
dynamicModelProvider.resolveModel is passing the literal "default" via modelName
which prevents dynamic default resolution; update the code around
dynamicModelProvider.resolveModel to normalize modelName (or the variable
normalizedProvider’s model hint) so that when modelName === "default" you pass
undefined instead (e.g., compute a lookupModel = modelName === "default" ?
undefined : modelName) before calling
dynamicModelProvider.resolveModel(normalizedProvider, lookupModel) so
createProvider will trigger the provider's dynamic default path.

}
}

const { tool, toolId } = this.resolveToolExecutionTarget(toolName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ambiguous lookups are still falling through the normal ToolResult path.

resolveToolExecutionTarget() now throws on duplicate names, but executeTool() only rethrows "not found in registry" / "not executable" precondition failures. This new ambiguity error therefore gets converted into { success: false }, so callers that treat a resolved executeTool() value as success — e.g. the exec flow in src/cli/commands/mcp.ts — won't surface the lookup failure correctly.

💡 Suggested follow-up in executeTool()
if (
  errMsg.includes("not found in registry") ||
  errMsg.includes("not executable") ||
  errMsg.includes("Ambiguous tool name")
) {
  throw error;
}

Also applies to: 668-689

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/mcp/toolRegistry.ts` at line 350, The executeTool flow currently
swallows the ambiguity error thrown by resolveToolExecutionTarget (e.g.,
"Ambiguous tool name") and converts it into a ToolResult failure; update
executeTool (and the similar block around the other occurrence) to detect and
rethrow ambiguity lookup errors instead of returning { success: false } — for
example, check the thrown error's message or type for "Ambiguous tool name" (in
addition to the existing "not found in registry" and "not executable" checks)
and rethrow the original error so callers relying on thrown lookup failures
(resolveToolExecutionTarget) receive the correct exception.

Comment on lines +574 to +593
let fetchInput: string | URL;
let fetchInit = { ...init };

if (input instanceof Request) {
fetchInput = input.url;
fetchInit = {
method: input.method,
headers: input.headers,
body: input.body,
...init,
};
} else {
fetchInput = input;
}

try {
const response = await fetchWithRetry(input, init);
const undici = await import("undici");
const response = await undici.fetch(fetchInput, {
...fetchInit,
dispatcher,
} as unknown as import("undici").RequestInit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Undici / Node.js fetch, if I already have new Request(url, { method: "POST", body: someReadableStream, duplex: "half" }), is it safe to proxy it via fetch(req.url, { method: req.method, headers: req.headers, body: req.body }), or do I need to forward the original Requestobject (or re-setduplex: "half" explicitly)?

💡 Result:

No, it is not safe to proxy via fetch(req.url, { method: req.method, headers: req.headers, body: req.body }) without explicitly setting duplex: "half". You need to either forward the original Request object directly with fetch(req) or explicitly set duplex: "half" in the options when using individual properties. The duplex property is required by the Fetch spec (and enforced in Node.js/Undici) when the request body is a ReadableStream. The original req has duplex: "half" set during construction. However, when passing individual init properties to fetch, the duplex option is not automatically copied from req.duplex or preserved. Omitting it will throw "RequestInit: duplex option is required when sending a body". Evidence from sources: - Node.js issue #46221: Explicitly requires duplex: "half" for stream bodies, with proxy/pipe examples adding it manually. - Undici issue #1626: Proxy examples use new Request(newUrl, { ..., body: req.body, duplex: "half" }). - WHATWG Fetch spec via MDN: duplex must be set for ReadableStream bodies. - Multiple PRs/issues (e.g., Undici #1681) confirm duplex addition and enforcement. Safest options: 1. fetch(req) - Forwards the entire Request, preserving duplex. 2. fetch(newUrl, { ...req, duplex: "half" }) - Explicitly sets it. 3. fetch(newUrl, { method: req.method, headers: req.headers, body: req.body, duplex: "half" }) - Manual copy with duplex.

Citations:


Preserve the original Request when proxying to avoid losing required duplex semantics.

When input is a Request, the current code reconstructs it as separate properties (body: input.body, etc.) passed to undici.fetch(). This loses the duplex property, which is required by the WHATWG Fetch spec and enforced in Node.js/Undici when the request body is a ReadableStream. Omitting it will throw RequestInit: duplex option is required when sending a body at runtime.

Pass the original Request object directly to preserve all streaming semantics:

Suggested direction
-      let fetchInput: string | URL;
-      let fetchInit = { ...init };
-
-      if (input instanceof Request) {
-        fetchInput = input.url;
-        fetchInit = {
-          method: input.method,
-          headers: input.headers,
-          body: input.body,
-          ...init,
-        };
-      } else {
-        fetchInput = input;
-      }
+      const fetchInput = input;
+      const fetchInit = { ...init };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/proxy/proxyFetch.ts` around lines 574 - 593, The proxy currently
reconstructs Request into fetchInput/fetchInit (variables fetchInput, fetchInit)
which strips Request-specific semantics like duplex; instead when input is an
instance of Request pass the original Request object through to undici.fetch
(i.e., call undici.fetch(input, { dispatcher }) or merge only allowed overrides)
so the duplex flag and streaming body are preserved; update the branch that
handles input instanceof Request to avoid creating a new RequestInit with
input.body and ensure undici.fetch receives the original Request instance (or a
properly constructed Request preserving duplex) along with the dispatcher.

Comment on lines +199 to +206
function truncateUtf8String(input: string, maxBytes: number): string {
if (utf8ByteLength(input) <= maxBytes) {
return input;
}
return `${input.slice(0, maxBytes)}${TRUNCATION_MARKER}`;

const markerBytes = utf8ByteLength(TRUNCATION_MARKER);
if (maxBytes <= 0 || maxBytes < markerBytes) {
return "";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the truncation marker visible when only a few bytes remain.

Returning "" here means the last overflow can silently cut data once the remaining budget is smaller than TRUNCATION_MARKER. Because both capped block content and raw-text capture reuse this helper, the final payload can look complete even though it was truncated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/proxy/sseInterceptor.ts` around lines 199 - 206, The current
truncateUtf8String returns "" when maxBytes <= 0 or less than the
TRUNCATION_MARKER size, which hides the truncation marker; instead, when
maxBytes > 0 but smaller than utf8ByteLength(TRUNCATION_MARKER) return a
byte-aware truncated slice of TRUNCATION_MARKER so at least part of the marker
is visible. Update truncateUtf8String to, in the branch that checks maxBytes and
markerBytes, if maxBytes <= 0 return "" but if 0 < maxBytes < markerBytes
produce a markerPrefix by iterating characters (using utf8ByteLength to track
bytes) until maxBytes is reached and return that markerPrefix; keep the existing
behavior for larger budgets and normal truncation logic.

Comment on lines +336 to +351
} catch (error) {
await this.restoreScheduledTask(existing, "update schedule rollback");
await this.rollbackTaskUpdate(taskId, existing, error);
throw TaskError.create(
"SCHEDULE_FAILED",
`Failed to update schedule for task ${taskId}`,
{
cause: error instanceof Error ? error : undefined,
details: {
taskId,
previousSchedule: existing.schedule,
attemptedSchedule,
},
},
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider reversing rollback order: store before schedule.

Currently, restoreScheduledTask is called before rollbackTaskUpdate. If restoreScheduledTask succeeds but rollbackTaskUpdate fails (and throws per line 534), the store will contain the new task state while the backend has the old schedule—causing a mismatch.

Rolling back the store first ensures the persisted state is consistent before attempting to restore the backend schedule.

🔧 Suggested fix
       } catch (error) {
-        await this.restoreScheduledTask(existing, "update schedule rollback");
         await this.rollbackTaskUpdate(taskId, existing, error);
+        await this.restoreScheduledTask(existing, "update schedule rollback");
         throw TaskError.create(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
await this.restoreScheduledTask(existing, "update schedule rollback");
await this.rollbackTaskUpdate(taskId, existing, error);
throw TaskError.create(
"SCHEDULE_FAILED",
`Failed to update schedule for task ${taskId}`,
{
cause: error instanceof Error ? error : undefined,
details: {
taskId,
previousSchedule: existing.schedule,
attemptedSchedule,
},
},
);
}
} catch (error) {
await this.rollbackTaskUpdate(taskId, existing, error);
await this.restoreScheduledTask(existing, "update schedule rollback");
throw TaskError.create(
"SCHEDULE_FAILED",
`Failed to update schedule for task ${taskId}`,
{
cause: error instanceof Error ? error : undefined,
details: {
taskId,
previousSchedule: existing.schedule,
attemptedSchedule,
},
},
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/tasks/taskManager.ts` around lines 336 - 351, The rollback currently
calls restoreScheduledTask(existing, ...) before rollbackTaskUpdate(taskId,
existing, error) which can leave the store inconsistent if rollbackTaskUpdate
fails; swap the two calls so you call await this.rollbackTaskUpdate(taskId,
existing, error) first, then await this.restoreScheduledTask(existing, "update
schedule rollback"); keep the TaskError.create(...) construction and the same
error/details payload (taskId, previousSchedule: existing.schedule,
attemptedSchedule) so the thrown error behavior is unchanged.

Comment on lines +518 to +536
private async rollbackTaskUpdate(
taskId: string,
previousTask: Task,
error: unknown,
): Promise<Task> {
try {
return await this.getStore().update(taskId, previousTask);
} catch (rollbackError) {
logger.error(
"[TaskManager] Failed to roll back task update — store and in-memory state may be diverged; manual reconciliation required",
{
taskId,
originalError: String(error),
rollbackError: String(rollbackError),
},
);
throw rollbackError;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Rollback failure swallows the original error context.

When rollbackTaskUpdate throws rollbackError (line 534), callers like resume() never reach their throw TaskError.create(...) lines. The user receives a generic store error instead of the typed SCHEDULE_FAILED error with schedule details.

Consider wrapping both errors to preserve context:

🔧 Suggested fix
   private async rollbackTaskUpdate(
     taskId: string,
     previousTask: Task,
     error: unknown,
   ): Promise<Task> {
     try {
       return await this.getStore().update(taskId, previousTask);
     } catch (rollbackError) {
       logger.error(
         "[TaskManager] Failed to roll back task update — store and in-memory state may be diverged; manual reconciliation required",
         {
           taskId,
           originalError: String(error),
           rollbackError: String(rollbackError),
         },
       );
-      throw rollbackError;
+      throw TaskError.create(
+        "SCHEDULE_FAILED",
+        `Rollback failed after scheduling error for task ${taskId}`,
+        {
+          cause: error instanceof Error ? error : undefined,
+          details: { taskId, rollbackError: String(rollbackError) },
+        },
+      );
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/tasks/taskManager.ts` around lines 518 - 536, The rollbackTaskUpdate
currently throws only rollbackError which hides the original error and prevents
callers like resume() from creating the proper TaskError (e.g.,
SCHEDULE_FAILED); change rollbackTaskUpdate to preserve both errors by
constructing and throwing a wrapped error that includes the original error
(error) and the rollback error (rollbackError) as structured context (e.g.,
message combining both and/or properties like originalError and rollbackError or
using the cause field) so callers can still detect and rethrow TaskError.create
with schedule details; update references in rollbackTaskUpdate and ensure
resume() and other callers can inspect the wrapped error to generate the correct
TaskError.

Comment on lines +106 to +111
const delegate =
typeof provider.getDelegate === "function"
? provider.getDelegate()
: provider._delegate;
const delegateName = delegate?.constructor?.name || "";
return Boolean(delegateName && delegateName !== "NoopTracerProvider");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In OpenTelemetry JS, before any SDK tracer provider is registered, can trace.getTracerProvider().getDelegate?.()return anotherProxyTracerProvider, and should that still be treated as the default no-op path rather than an external provider?

💡 Result:

Yes, before any SDK TracerProvider is registered via trace.setGlobalTracerProvider, calling trace.getTracerProvider returns a ProxyTracerProvider. This ProxyTracerProvider has a getDelegate?. method that returns its internal delegate, which defaults to a no-op TracerProvider (such as NoopTracerProvider) until an SDK provider is set. This should be treated as the default no-op path, not an external provider, because the delegate is the built-in no-op implementation from @opentelemetry/api, ensuring no telemetry is emitted until a real SDK is registered. Once an SDK calls register or setGlobalTracerProvider, the proxy delegates to that real provider, activating early-acquired tracers without reinitialization.

Citations:


🏁 Script executed:

# First, locate the telemetry service file
fd -t f telemetryService.ts

Repository: juspay/neurolink

Length of output: 97


🏁 Script executed:

# Read the relevant section of the file
cat -n src/lib/telemetry/telemetryService.ts | sed -n '95,125p'

Repository: juspay/neurolink

Length of output: 1286


🏁 Script executed:

# Search for related delegate and provider checks in the codebase
rg -A 5 -B 5 "hasExternalTracerProvider|getDelegate" src/lib/telemetry/

Repository: juspay/neurolink

Length of output: 3845


🏁 Script executed:

# Look for tests related to external provider detection
fd -t f -i "test|spec" | xargs rg -l "hasExternalTracerProvider|ProxyTracerProvider" 2>/dev/null | head -20

Repository: juspay/neurolink

Length of output: 42


Align delegate-branch provider-name exclusions with top-level check.

The top-level branch excludes both ProxyTracerProvider and NoopTracerProvider, but the delegate branch only excludes NoopTracerProvider. If getDelegate() ever returns another proxy wrapper, this inconsistency causes a false-positive external-provider detection and unnecessary provider replacement.

🔧 Proposed fix
-      return Boolean(delegateName && delegateName !== "NoopTracerProvider");
+      return Boolean(
+        delegateName &&
+          delegateName !== "NoopTracerProvider" &&
+          delegateName !== "ProxyTracerProvider",
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/telemetry/telemetryService.ts` around lines 106 - 111, The
delegate-branch check that computes delegateName from
provider.getDelegate()/provider._delegate only excludes "NoopTracerProvider" but
must match the top-level exclusion of both "NoopTracerProvider" and
"ProxyTracerProvider"; update the boolean return to also exclude
"ProxyTracerProvider" (i.e., require delegateName && delegateName !==
"NoopTracerProvider" && delegateName !== "ProxyTracerProvider") so a proxy
wrapper returned by getDelegate() won't be misdetected as an external provider
(referencing provider.getDelegate, provider._delegate, delegateName, and the
provider names).

…corer definitions and unit tests

- Decompose claudeProxyRoutes.ts: extract 20+ named handlers (handleTranslatedClaudeRequest,
  handleClaudePassthroughRequest, loadClaudeProxyAccounts, buildAnthropicTerminalErrorResponse,
  createAnthropicAttemptLogger, prepareAnthropicAccountAttempt, etc.) to resolve max-lines-per-function
- Decompose neurolink.ts: extract executeGenerateWithMetricsContext, prepareGenerateRequest,
  buildGenerateTextOptions, maybeHandleEarlyGenerateResult, and 8+ more private methods
- Decompose proxy CLI: extract ensureProxyStartAllowed, loadProxyStartEnv, createProxyNeurolinkRuntime
- Decompose baseProvider: extract runGenerateInActiveContext private method, add gen() alias
- Decompose GoogleVertex: extract maybeExecuteNativeGemini3ToolStream and executeAISDKStream
- Add AIProviderFactory.resolveModelFromEnvironment for per-provider model env var overrides
- Refactor MCPToolRegistry: extract resolveToolExecutionTarget and createExecutionContext helpers
- Refactor oauthFetch: extract resolveOAuthRequestUrl helper, clean up request flow
- Fix proxyFetch: add mergeTraceHeaders, pass input to injectTraceContext to preserve request headers
- Fix OpenAI: resolve toolChoice once before stream creation to avoid duplicate computation
- Fix TaskManager: cleanup callbacks before store delete, rollback on schedule failure, defer history clear
- Add built-in LLM scorer definitions to ScorerRegistry (hallucination, toxicity, and more)
- Add new proxy types: ProxyBodyCaptureInput, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder
- Update observability instrumentation, request logger, docs site, and dependencies
- Ignore pre-existing lodash vulnerabilities in @semantic-release dev deps (no patch available)
@murdore
murdore force-pushed the refactor/decompose-proxy-routes-neurolink-core branch from 1098d44 to 8e88aa5 Compare April 2, 2026 07:39
@github-actions

github-actions Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: 8e88aa5b414ef508c6d704d412cb8f46dde7f141
  • Message: refactor(proxy): decompose large functions for lint compliance, add scorer definitions and unit tests
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

@github-actions

github-actions Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore
murdore merged commit af7862b into release Apr 2, 2026
16 checks passed
@murdore
murdore deleted the refactor/decompose-proxy-routes-neurolink-core branch April 2, 2026 09:45
@github-actions

github-actions Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 9.43.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — 8e88aa5b Deployed Apr 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants