Skip to content

fix(observability): enrich NoOutputGeneratedError sentinel chunk metadata - #990

Merged
murdore merged 1 commit into
releasefrom
fix/curator-issue-06-no-output-context
Apr 27, 2026
Merged

murdore merged 1 commit into
releasefrom
fix/curator-issue-06-no-output-context

Conversation

@murdore

@murdore murdore commented Apr 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Curator P3-6: when the AI SDK throws NoOutputGeneratedError, the sentinel chunk yielded by StreamHandler.createTextStream() previously carried only { noOutput: true, errorType }. Downstream telemetry couldn't tell why the stream produced no output.

Reproduction (before fix)

[FAIL] 6.0 STATIC artifact shape — sentinel missing
       [finishReason, usage, providerError]; present=[noOutput, errorType]

After fix:

[PASS] 6.0 STATIC artifact shape — sentinel enriched: present=[noOutput, errorType,
       finishReason, usage, providerError, modelResponseRaw]

Fix

src/lib/core/modules/StreamHandler.ts — in the NoOutputGeneratedError catch block:

  1. Best-effort await of result.finishReason and result.totalUsage (AI SDK's StreamTextResult getters). They reject today; if a future SDK version surfaces partial values the sentinel automatically carries them.
  2. Default finishReason: "error" and zero-usage when those getters reject.
  3. Capture error.message as providerError, error.cause (truncated to 500 chars) as modelResponseRaw.
  4. Stamp the active OTel span with an enriched langfuse.status_message:
    Stream produced no output (NoOutputGeneratedError):
      finishReason=error, promptTokens=0, completionTokens=0
    

Type signature change

createTextStream now accepts optional finishReason and totalUsage:

createTextStream(result: {
  textStream: AsyncIterable<string>;
  finishReason?: Promise<unknown> | unknown;
  totalUsage?: Promise<unknown> | unknown;
}): AsyncGenerator<{ content: string }>;

Existing callers don't need changes — both new fields are optional, and providers passing the AI SDK's full StreamTextResult automatically benefit.

Backward compatibility

Additive only. Listeners that check noOutput === true continue to work unchanged.

Verification

pnpm run build
npx tsx test/continuous-test-suite-issue-06-no-output-context.ts

Expected: Results: 1 passed, 0 failed, 18 skipped. The 18 SKIPs are dynamic-trigger recipes that don't fire NoOutputGeneratedError on the configured providers — the AI SDK only throws when zero text-delta parts are emitted, which is rare in practice. The static check is the deterministic reproduction.

Test plan

  • Static artifact check passes against fixed release
  • Sentinel carries all 6 keys (noOutput, errorType, finishReason, usage, providerError, modelResponseRaw)
  • OTel span receives enriched langfuse.status_message
  • No regression on noOutput === true predicate (still set)

Summary by CodeRabbit

  • Bug Fixes

    • Improved detection and standardized handling of scenarios where the AI model produces no output across all supported providers.
    • Enhanced error information with upstream provider context for better debugging and diagnostics.
  • Tests

    • Added comprehensive end-to-end test suite validating no-output error handling, sentinel enrichment, and propagation across provider implementations.

@vercel

vercel Bot commented Apr 25, 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 27, 2026 2:20am

Copilot AI review requested due to automatic review settings April 25, 2026 10:44
@coderabbitai

coderabbitai Bot commented Apr 25, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@murdore has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 31 minutes and 57 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 86f21ceb-9e74-4990-bcf4-77e6da8d286d

📥 Commits

Reviewing files that changed from the base of the PR and between 47bf52f and bfa23c8.

📒 Files selected for processing (25)
  • .mcp-config.json
  • src/lib/core/baseProvider.ts
  • src/lib/core/modules/StreamHandler.ts
  • src/lib/neurolink.ts
  • src/lib/providers/anthropic.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/azureOpenai.ts
  • src/lib/providers/googleAiStudio.ts
  • src/lib/providers/googleVertex.ts
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/mistral.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/openRouter.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/services/server/ai/observability/instrumentation.ts
  • src/lib/types/index.ts
  • src/lib/types/noOutputSentinel.ts
  • src/lib/types/stream.ts
  • src/lib/utils/noOutputSentinel.ts
  • test/continuous-test-suite-autoresearch-live.ts
  • test/continuous-test-suite-context.ts
  • test/continuous-test-suite-dynamic.ts
  • test/continuous-test-suite-issue-06-no-output-context.ts
  • test/continuous-test-suite-proxy.ts

Walkthrough

This PR introduces a centralized "no output" sentinel handling system across the streaming pipeline. It replaces provider-local error handling with shared utilities (buildNoOutputSentinel, detectPostStreamNoOutput, stampNoOutputSpan) that consistently enrich sentinel chunks with upstream provider errors and token usage. All six providers now capture upstream failures, detect no-output scenarios post-stream, and the wrapper distinguishes sentinel chunks from real output when gating fallback behavior.

Changes

Cohort / File(s) Summary
Sentinel Infrastructure
src/lib/types/noOutputSentinel.ts, src/lib/types/index.ts, src/lib/types/stream.ts
Adds NoOutputSentinel and NoOutputSentinelResultLike types; extends StreamResult.stream union to include sentinel chunks; re-exports types via index.
Shared Utilities
src/lib/utils/noOutputSentinel.ts
New module providing buildNoOutputSentinel, detectPostStreamNoOutput, stampNoOutputSpan, and buildNoOutputStatusMessage for constructing, detecting, stamping, and logging no-output scenarios with enriched provider context.
Core Stream Handler
src/lib/core/modules/StreamHandler.ts
Replaces provider-local sentinel stamping with calls to shared buildNoOutputSentinel + stampNoOutputSpan; adds post-stream no-output detection via detectPostStreamNoOutput when no chunks produced; prevents double-sentinel yields on NoOutputGeneratedError.
Provider Stream Handlers
src/lib/providers/anthropicBaseProvider.ts, src/lib/providers/openAI.ts, src/lib/providers/openRouter.ts, src/lib/providers/openaiCompatible.ts, src/lib/providers/huggingFace.ts, src/lib/providers/litellm.ts
Each provider now: captures upstream streamText errors via onError, tracks actual content chunks (excluding control events), constructs enriched sentinels on NoOutputGeneratedError, invokes post-stream detection when zero chunks yielded, and stamps telemetry spans.
Stream Wrapper
src/lib/neurolink.ts
Distinguishes sentinel chunks from real output; counts only non-sentinel chunks with text content or media payloads; gates fallback on realOutputChunks === 0 instead of raw chunk count, preventing false fallback for audio/image-only streams.
Observability
src/lib/services/server/ai/observability/instrumentation.ts
Preserves upstream-enriched langfuse.status_message instead of unconditionally overwriting; conditionally applies generic message only when status is missing/invalid.
Test Suite Updates
test/continuous-test-suite-autoresearch-live.ts, test/continuous-test-suite-context.ts, test/continuous-test-suite-dynamic.ts, test/continuous-test-suite-proxy.ts
Updates model resolution to honor per-provider environment variables (ANTHROPIC_MODEL, OPENAI_MODEL, VERTEX_MODEL, TEST_MODEL) with fallback defaults.
Comprehensive No-Output Test Suite
test/continuous-test-suite-issue-06-no-output-context.ts
New end-to-end test suite (1135 LOC) validating sentinel enrichment, post-stream detection, span stamping, shared utility correctness, provider error capture, wrapper filtering, and artifact wiring across all providers and handlers.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Provider as Provider Stream
    participant StreamHandler
    participant Utilities as Sentinel Utils
    participant Wrapper as Wrapper (neurolink)

    Client->>Provider: initiate stream
    Provider->>Provider: onError callback (capture upstream error)
    
    loop streaming chunks
        Provider-->>StreamHandler: yield chunk/NoOutputGeneratedError
        StreamHandler->>StreamHandler: count content chunks
        alt NoOutputGeneratedError caught
            StreamHandler->>Utilities: buildNoOutputSentinel(error, captured)
            Utilities->>Utilities: enrich with provider error, usage
            Utilities-->>StreamHandler: NoOutputSentinel
            StreamHandler->>Utilities: stampNoOutputSpan(sentinel)
            StreamHandler-->>Wrapper: yield sentinel
        else chunk received
            StreamHandler-->>Wrapper: yield chunk
        end
    end

    alt no chunks yielded
        StreamHandler->>Utilities: detectPostStreamNoOutput(result, captured)
        Utilities->>Utilities: await finishReason (detect rejection)
        Utilities-->>StreamHandler: sentinel (if rejected)
        StreamHandler->>Utilities: stampNoOutputSpan(sentinel)
        StreamHandler-->>Wrapper: yield sentinel
    end

    Wrapper->>Wrapper: filter chunks (exclude sentinels)
    Wrapper->>Wrapper: count real output (text/media only)
    alt realOutputChunks === 0
        Wrapper->>Wrapper: trigger fallback
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

released

Suggested reviewers

  • Pdogra2520
  • pdogra1299

Poem

🐰 A sentinel hops through the stream,
Catching no-output's silent scream,
Each provider now enriches with care,
Stamps the span, and files it fair! 🌟
No more doubles, no more lost—
The rabbit's refactor has paid its cost! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main changeset: enriching NoOutputGeneratedError sentinel chunk metadata with finishReason, usage, providerError, and modelResponseRaw fields across StreamHandler and all streaming providers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this 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 fix/curator-issue-06-no-output-context

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.

@github-actions

github-actions Bot commented Apr 25, 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: bfa23c8990d042d64d4e771d6ca66e88bd77f758
  • Message: fix(observability): enrich NoOutputGeneratedError sentinel chunk metadata + actually trigger the catch path the production bug needs
  • 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

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

@github-actions

Copy link
Copy Markdown
Contributor

Documentation Validation Results

🚀 Documentation validation passed!

Check Status Result
Frontmatter Validation ✅ Passed
TypeScript Check ✅ Passed
Build ✅ Passed
Link Validation ✅ Passed

📦 Build artifact uploaded successfully. Ready for deployment preview.

Commit: 4ac257def27c4c3664769c959ab9ef05017af563 | Workflow: View logs

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

Enhances observability for zero-output streaming failures by enriching the NoOutputGeneratedError sentinel chunk metadata emitted from StreamHandler.createTextStream(), enabling downstream telemetry to understand why a stream produced no output.

Changes:

  • Enrich NoOutputGeneratedError sentinel chunk metadata with finishReason, usage, providerError, and modelResponseRaw, and stamp the active OTel span with a status message.
  • Add a continuous test script (plus env/credential guard helpers) to verify the enriched sentinel metadata in the compiled artifact and via best-effort live provider reproduction.
  • Add documentation describing the Curator Issue #6 report, fix, and verification steps.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/lib/core/modules/StreamHandler.ts Adds best-effort metadata extraction in the NoOutputGeneratedError path and enriches span attributes + sentinel chunk metadata.
test/helpers/envGuard.ts Adds helpers for skipping tests when env vars are missing and classifying expected provider/credential errors.
test/continuous-test-suite-issue-06-no-output-context.ts Adds a reproduction/verification script (static artifact scan + live provider recipes) for Issue #6.
docs/curator-feedback-fixes/issue-06-no-output-context.md Documents the issue, fix, and verification procedure.

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


> When AI SDK throws `NoOutputGeneratedError`, the sentinel chunk yielded
> by `StreamHandler.createTextStream` carries only `{ noOutput: true,
errorType: "NoOutputGeneratedError" }` — no `finishReason`, no `usage`,

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

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

The blockquote in the Curator report is broken across lines: the wrapped errorType: "NoOutputGeneratedError" line is missing the leading > marker, so it won’t render as part of the quote. Add > to the continuation line (or keep the snippet on one line) to preserve the intended formatting.

Suggested change
errorType: "NoOutputGeneratedError" }` — no `finishReason`, no `usage`,
> errorType: "NoOutputGeneratedError" }` — no `finishReason`, no `usage`,

Copilot uses AI. Check for mistakes.
Comment thread src/lib/core/modules/StreamHandler.ts Outdated
promptTokens?: number;
completionTokens?: number;
};
const summary = `Stream produced no output (NoOutputGeneratedError): finishReason=${String(finishReason)}, promptTokens=${u?.promptTokens ?? 0}, completionTokens=${u?.completionTokens ?? 0}`;

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

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

langfuse.status_message set here is likely overwritten on span end by ContextEnricher.applyNonErrorLangfuseLevel() when it sees neurolink.no_output === true (it unconditionally sets langfuse.status_message to a generic string). To preserve the enriched message, either set langfuse.level here (e.g. WARNING) so applyNonErrorLangfuseLevel won’t run, or update the enricher to only set langfuse.status_message if it isn’t already present.

Suggested change
const summary = `Stream produced no output (NoOutputGeneratedError): finishReason=${String(finishReason)}, promptTokens=${u?.promptTokens ?? 0}, completionTokens=${u?.completionTokens ?? 0}`;
const summary = `Stream produced no output (NoOutputGeneratedError): finishReason=${String(finishReason)}, promptTokens=${u?.promptTokens ?? 0}, completionTokens=${u?.completionTokens ?? 0}`;
activeSpan.setAttribute("langfuse.level", "WARNING");

Copilot uses AI. Check for mistakes.
Comment thread src/lib/core/modules/StreamHandler.ts Outdated
Comment on lines +173 to +190
let finishReason: unknown = "error";
let usage: unknown = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
};
try {
if (result.finishReason !== undefined) {
finishReason = await Promise.resolve(result.finishReason);
}
} catch {
// Expected — AI SDK rejects it on no-output. Keep "error" default.
}
try {
if (result.totalUsage !== undefined) {
usage = await Promise.resolve(result.totalUsage);
}
} catch {

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

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

The new totalUsage field doesn’t appear to exist on the AI SDK StreamTextResult used throughout the codebase (providers and analytics collector use result.usage). As a result this branch will almost always keep the default object and won’t benefit from future SDK improvements. Consider accepting/reading usage (and normalizing it to the repo’s TokenUsage shape via extractTokenUsage/createEmptyTokenUsage) instead of introducing totalUsage + a {promptTokens,...} default that doesn’t match the {input, output, total} convention used elsewhere (e.g. stream span enrichment).

Copilot uses AI. Check for mistakes.
console.log(
`\n${colors.bright}Results:${colors.reset} ${passed} passed, ${failed} failed, ${skipped} skipped`,
);
process.exit(0); // bug repro: failed > 0 expected

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

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

This script always exits with code 0 even when one or more checks record FAIL, which makes it easy for regressions to slip by in CI/automation. Consider exiting non-zero when failed > 0 (similar to other continuous test suite scripts) so the verification command in the PR description actually enforces the expected behavior.

Suggested change
process.exit(0); // bug repro: failed > 0 expected
process.exit(failed > 0 ? 1 : 0);

Copilot uses AI. Check for mistakes.
@murdore
murdore force-pushed the fix/curator-issue-06-no-output-context branch from 265978b to 80b90a5 Compare April 25, 2026 22:02
@murdore

murdore commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed addressing reviewer Finding #6 + applying same recipe as #988/#989:

  • Rebased onto latest origin/release.
  • Dropped the per-issue doc.
  • Finding Fix/ci pipeline #6 fix: extracted the sentinel build into a shared helper src/lib/utils/noOutputSentinel.ts (buildNoOutputSentinel(error, result?) + buildNoOutputStatusMessage(...)). All four providers that previously return-ed silently on NoOutputGeneratedError — openAI.ts:691, anthropicBaseProvider.ts:252, huggingFace.ts:257, openRouter.ts:511 — now yield the enriched sentinel before returning. StreamHandler.createTextStream was refactored to use the same helper so every code path produces an identical shape.

Types live in src/lib/types/noOutputSentinel.ts (NoOutputSentinel + NoOutputSentinelResultLike) per CLAUDE.md rule 2.

LiteLLM intentionally still rethrows — its inline comment at line 547 says "propagating to fallback chain" and that's a deliberate choice for fallback orchestration, not a bug. Left untouched.

Static artifact check now reads dist/lib/utils/noOutputSentinel.js (single source of truth) instead of StreamHandler.js. Verified: Results: 1 passed, 0 failed, 18 skipped.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown

@murdore Sure! Kicking off a full review of the updated PR now. The refactoring into a shared buildNoOutputSentinel helper with consistent types looks like a solid approach — will verify the four provider call sites, the StreamHandler refactor, the LiteLLM intentional rethrow, and the updated static artifact check path.


🧠 Learnings used
Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-17T04:30:25.521Z
Learning: In neurolink (src/lib/neurolink.ts), the `pipelineAHandled: true` flag on `generation:end` events is set ONLY by `finalizeGenerateRequestResult` (the SDK-level wrapper). Native providers (amazonBedrock.ts, ollama.ts, googleAiStudio.ts, googleVertex.ts) emit their own `generation:end` events WITHOUT this flag. The `initializeMetricsListeners()` early-return guard `if (data.pipelineAHandled) return` correctly prevents duplicates: it skips the SDK-level event while allowing native provider events through to Pipeline B.
✅ Actions performed

Full review triggered.

@github-actions

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 commented Apr 26, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed addressing reviewer Finding #6 follow-up:

Two providers were still uncovered by the previous round:

  • litellm.ts:545-552 rethrew NoOutputGeneratedError (intentional for fallback chain) but skipped enrichment. Now yields the sentinel before the rethrow — observability gets the structured context, fallback chain still triggers on the subsequent throw.
  • openaiCompatible.ts:331-338 silently return-ed. Now yields the sentinel before returning, matching the OpenAI / Anthropic / HuggingFace / OpenRouter pattern.

All five provider stream-transformers now go through the shared buildNoOutputSentinel helper. The static check still passes (Results: 1 passed, 0 failed, 18 skipped).

@coderabbitai full review

@murdore
murdore force-pushed the fix/curator-issue-06-no-output-context branch from 80b90a5 to adfa898 Compare April 26, 2026 06:03
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown

@murdore Sure! Kicking off a full review of the updated PR. The approach of yielding the sentinel before the rethrow in litellm.ts is clever — observability gets the enriched context while the fallback chain still triggers as intended. Will verify both new provider sites against the shared buildNoOutputSentinel helper, the yield-then-rethrow pattern in litellm.ts, and confirm all five providers are now consistent.


🧠 Learnings used
Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-17T04:30:25.521Z
Learning: In neurolink (src/lib/neurolink.ts), the `pipelineAHandled: true` flag on `generation:end` events is set ONLY by `finalizeGenerateRequestResult` (the SDK-level wrapper). Native providers (amazonBedrock.ts, ollama.ts, googleAiStudio.ts, googleVertex.ts) emit their own `generation:end` events WITHOUT this flag. The `initializeMetricsListeners()` early-return guard `if (data.pipelineAHandled) return` correctly prevents duplicates: it skips the SDK-level event while allowing native provider events through to Pipeline B.

Learnt from: Yaswanth-2874
Repo: juspay/neurolink PR: 880
File: src/lib/neurolink.ts:6191-6195
Timestamp: 2026-03-17T11:20:55.110Z
Learning: In neurolink stream fallback logging (src/lib/neurolink.ts handleStreamFallback), typical usage is to override both provider and model together, but logs should still accurately reflect one-sided overrides when only model or only provider is set.

Learnt from: YaswanthKurapati24
Repo: juspay/neurolink PR: 767
File: src/lib/core/baseProvider.ts:547-590
Timestamp: 2026-01-09T07:07:12.843Z
Learning: In the BaseProvider structured output orchestration (src/lib/core/baseProvider.ts), when Phase 3 (generateObject) fails, the code intentionally returns repairResult.json (from Phase 2) even if malformed, with structuredOutputAchieved: false. This "best effort" approach gives end users the most refined attempt at structured output along with a failure signal, allowing them to decide how to handle it.
✅ Actions performed

Full review triggered.

@github-actions

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 force-pushed the fix/curator-issue-06-no-output-context branch from adfa898 to 318eab7 Compare April 26, 2026 06:32
@murdore

murdore commented Apr 26, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed addressing the reviewer's three follow-up concerns:

  1. modelResponseRaw is no longer undefined: when the AI SDK error has no cause, the helper now falls back to ${error.name}: ${error.message} (truncated to 500 chars) so the field is always a string. The live test's has(k) = meta[k] !== undefined check now passes for this key on every sentinel.

  2. LiteLLM yield-then-rethrow no longer loses chunks in the test: the live verifier's tryRecipe previously caught the rethrow and returned {skip}, discarding the sentinel chunks the for-await had already pushed. Updated the catch block: if any collected chunk has noOutput: true, return the chunks array (preserves the observability contract); only fall back to SKIP when no usable chunk was yielded.

  3. Suite exits non-zero on failure: process.exit(0) → process.exit(failed === 0 ? 0 : 1) so CI actually enforces the enriched-sentinel contract.

Static check still PASS; live recipes still SKIP unless a provider trip fires (rare path, expected).

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown

@murdore Sure! Triggering a full review of the latest force-push. All three follow-up fixes look well-reasoned at a glance:

  1. The modelResponseRaw fallback to ${error.name}: ${error.message} is a clean guarantee that the field is always a non-undefined string, regardless of whether error.cause is populated — solid defensive choice.
  2. The tryRecipe catch-block fix correctly preserves sentinel chunks for the LiteLLM yield-then-rethrow path — the noOutput: true predicate as the discriminator is the right signal to use.
  3. process.exit(failed === 0 ? 0 : 1) is the correct CI-enforcing pattern.

Will do a thorough pass over all five provider sites, the shared helper, the type definitions, and the updated test suite now.


🧠 Learnings used
Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-17T04:30:25.521Z
Learning: In neurolink (src/lib/neurolink.ts), the `pipelineAHandled: true` flag on `generation:end` events is set ONLY by `finalizeGenerateRequestResult` (the SDK-level wrapper). Native providers (amazonBedrock.ts, ollama.ts, googleAiStudio.ts, googleVertex.ts) emit their own `generation:end` events WITHOUT this flag. The `initializeMetricsListeners()` early-return guard `if (data.pipelineAHandled) return` correctly prevents duplicates: it skips the SDK-level event while allowing native provider events through to Pipeline B.

Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-26T06:27:32.928Z
Learning: In juspay/neurolink PR `#989`, the `generation:end` dedup for `sdk.stream()` uses a per-call options-based context: `enhancedOptions._streamDedupContext` (type `StreamGenerationEndContext = { providerEmitted: boolean }`) is created fresh per `sdk.stream()` invocation. Native providers (googleVertex.ts, googleAiStudio.ts) call `markStreamProviderEmittedGenerationEnd(options)` before their emit; the `runStandardStreamRequest` finally block reads `dedupContext.providerEmitted` and skips the orchestration emit when already true. amazonBedrock.ts and ollama.ts have independent stream paths that never enter `runStandardStreamRequest` so they do not need instrumentation. The orchestration emit carries `pipelineAHandled: true` while native provider emits do not.

Learnt from: Yaswanth-2874
Repo: juspay/neurolink PR: 880
File: src/lib/neurolink.ts:6191-6195
Timestamp: 2026-03-17T11:20:55.110Z
Learning: In neurolink stream fallback logging (src/lib/neurolink.ts handleStreamFallback), typical usage is to override both provider and model together, but logs should still accurately reflect one-sided overrides when only model or only provider is set.

Learnt from: YaswanthKurapati24
Repo: juspay/neurolink PR: 767
File: src/lib/core/baseProvider.ts:547-590
Timestamp: 2026-01-09T07:07:12.843Z
Learning: In the BaseProvider structured output orchestration (src/lib/core/baseProvider.ts), when Phase 3 (generateObject) fails, the code intentionally returns repairResult.json (from Phase 2) even if malformed, with structuredOutputAchieved: false. This "best effort" approach gives end users the most refined attempt at structured output along with a failure signal, allowing them to decide how to handle it.
✅ Actions performed

Full review triggered.

@github-actions

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 commented Apr 26, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed: static verifier now also requires modelResponseRaw for full regression coverage of the sentinel contract.

The helper already populates modelResponseRaw unconditionally (falls back to ${error.name}: ${error.message} truncated to 500 chars when AI SDK doesn't surface a cause). The test was previously checking only finishReason, usage, providerError — a regression that broke modelResponseRaw would have slipped through.

Verified: Results: 1 passed, 0 failed, 18 skipped.

@coderabbitai full review

@murdore
murdore force-pushed the fix/curator-issue-06-no-output-context branch from 318eab7 to 9795578 Compare April 26, 2026 06:49
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown

@murdore Sure! Triggering a full review of the latest force-push. Closing the regression gap on modelResponseRaw is the right call — since the helper already guarantees the field unconditionally, having the static verifier enforce all six sentinel keys (noOutput, errorType, finishReason, usage, providerError, modelResponseRaw) means any future accidental omission will be caught deterministically at CI time rather than silently slipping through.


🧠 Learnings used
Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-17T04:30:25.521Z
Learning: In neurolink (src/lib/neurolink.ts), the `pipelineAHandled: true` flag on `generation:end` events is set ONLY by `finalizeGenerateRequestResult` (the SDK-level wrapper). Native providers (amazonBedrock.ts, ollama.ts, googleAiStudio.ts, googleVertex.ts) emit their own `generation:end` events WITHOUT this flag. The `initializeMetricsListeners()` early-return guard `if (data.pipelineAHandled) return` correctly prevents duplicates: it skips the SDK-level event while allowing native provider events through to Pipeline B.

Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-26T06:27:32.928Z
Learning: In juspay/neurolink PR `#989`, the `generation:end` dedup for `sdk.stream()` uses a per-call options-based context: `enhancedOptions._streamDedupContext` (type `StreamGenerationEndContext = { providerEmitted: boolean }`) is created fresh per `sdk.stream()` invocation. Native providers (googleVertex.ts, googleAiStudio.ts) call `markStreamProviderEmittedGenerationEnd(options)` before their emit; the `runStandardStreamRequest` finally block reads `dedupContext.providerEmitted` and skips the orchestration emit when already true. amazonBedrock.ts and ollama.ts have independent stream paths that never enter `runStandardStreamRequest` so they do not need instrumentation. The orchestration emit carries `pipelineAHandled: true` while native provider emits do not.

Learnt from: Yaswanth-2874
Repo: juspay/neurolink PR: 880
File: src/lib/neurolink.ts:6191-6195
Timestamp: 2026-03-17T11:20:55.110Z
Learning: In neurolink stream fallback logging (src/lib/neurolink.ts handleStreamFallback), typical usage is to override both provider and model together, but logs should still accurately reflect one-sided overrides when only model or only provider is set.
✅ Actions performed

Full review triggered.

@github-actions

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 commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

Aggregate validation against Vertex/Gemini-3.1-Pro and Anthropic/Sonnet-4.6

Ran every continuous test suite (37 files, 56 suite-runs across two model passes + a no-AI pass) against this branch. Findings:

PR-scope test passes cleanly:

[PASS] 6.0 STATIC: shipped sentinel literal contains all 6 enriched keys
[PASS] 6.1 (×7) every wired site uses buildNoOutputSentinel
[PASS] 6.2 RUNTIME: helper produces all 6 keys with correct types
[PASS] 6.3 RUNTIME: helper reads partial values from result-like
[PASS] 6.4 RUNTIME: helper extracts error.cause into modelResponseRaw
[PASS] 6.5 END-TO-END: local connection-kill triggers enriched sentinel
[PASS] 6.6 REGRESSION: StreamHandler does not yield duplicate sentinels
[PASS] 6.7 REGRESSION: Pipeline B preserves enriched langfuse.status_message
[PASS] 6.8 REGRESSION: OpenRouter/LiteLLM gate on contentYielded
[PASS] 6.9 (×7) every wired site stamps OTel span via stampNoOutputSpan
[PASS] 6.10 REGRESSION: status message handles AI SDK v6 usage shape
[PASS] 6.11 REGRESSION: wrapper excludes sentinel from fallback gate
[PASS] 6.12 REGRESSION: media chunks count as real output
[PASS] 6.13 REGRESSION: helper accepts underlyingError parameter
[PASS] 6.14 (×6) providers capture onError and pass underlyingError

Results: 32 passed, 0 failed, 3 skipped, exit=0

Wider suite (40 of 56 suite-runs PASS): the 16 residual failures are all categorized as non-PR causes:

  • Anthropic credit-balance (Pass C)
  • OpenAI insufficient_quota (issue-04 stream sub-test on OpenAI)
  • OpenRouter gemma-3-4b-it:free no system-prompt support
  • Pre-existing AsyncLocalStorage / observability wrapper bug (generate() returns empty inside setLangfuseContext callback)
  • Circuit-breaker half-open unit-test race
  • LLM nondeterminism (RAGAS judges, business-tool metric mentions)
  • Local launchd com.neurolink.proxy already registered

None of the residual failures intersect the PR's surface: buildNoOutputSentinel, detectPostStreamNoOutput, realOutputChunks gate, stampNoOutputSpan, Pipeline B langfuse.status_message preservation, or the per-provider onError capture.

This commit also includes a separate test-hygiene cleanup (no functional code changes) so the broader suite respects model env vars consistently — context.ts, dynamic.ts, autoresearch-live.ts, and proxy.ts previously hardcoded model identifiers; they now read VERTEX_MODEL / ANTHROPIC_MODEL / OPENAI_MODEL like the rest of the suite. media-gen.ts keeps gemini-2.5-flash-image per its image-generation requirement.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown

@murdore This is an exceptionally thorough validation pass. A few observations before I kick off the full review:

  • 15 PR-scope categories all green with 32/32 passing — the coverage arc from static artifact shape → per-site wiring (×7) → runtime helper contract → end-to-end connection-kill → 9 regression guards is exactly the right shape for a change like this.
  • 16 residual failures correctly scoped out: the categorization is clean — credit/quota exhaustion, the pre-existing AsyncLocalStorage/observability wrapper bug, circuit-breaker race, RAGAS nondeterminism, and local launchd conflict are all environmental or pre-existing, none touching buildNoOutputSentinel / detectPostStreamNoOutput / stampNoOutputSpan / Pipeline B path.
  • Test-hygiene cleanup looks sound: replacing hardcoded model identifiers with VERTEX_MODEL / ANTHROPIC_MODEL / OPENAI_MODEL env-var reads in context.ts, dynamic.ts, autoresearch-live.ts, and proxy.ts is the right consistency fix. The media-gen.ts exception for gemini-2.5-flash-image is appropriate given the image-generation hard-requirement on that specific model.

Triggering the full review now.


🧠 Learnings used
Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-17T04:30:25.521Z
Learning: In neurolink (src/lib/neurolink.ts), the `pipelineAHandled: true` flag on `generation:end` events is set ONLY by `finalizeGenerateRequestResult` (the SDK-level wrapper). Native providers (amazonBedrock.ts, ollama.ts, googleAiStudio.ts, googleVertex.ts) emit their own `generation:end` events WITHOUT this flag. The `initializeMetricsListeners()` early-return guard `if (data.pipelineAHandled) return` correctly prevents duplicates: it skips the SDK-level event while allowing native provider events through to Pipeline B.

Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-26T06:27:32.928Z
Learning: In juspay/neurolink PR `#989`, the `generation:end` dedup for `sdk.stream()` uses a per-call options-based context: `enhancedOptions._streamDedupContext` (type `StreamGenerationEndContext = { providerEmitted: boolean }`) is created fresh per `sdk.stream()` invocation. Native providers (googleVertex.ts, googleAiStudio.ts) call `markStreamProviderEmittedGenerationEnd(options)` before their emit; the `runStandardStreamRequest` finally block reads `dedupContext.providerEmitted` and skips the orchestration emit when already true. amazonBedrock.ts and ollama.ts have independent stream paths that never enter `runStandardStreamRequest` so they do not need instrumentation. The orchestration emit carries `pipelineAHandled: true` while native provider emits do not.
✅ Actions performed

Full review triggered.

@github-actions

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

Comment thread test/continuous-test-suite-issue-06-no-output-context.ts Dismissed

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
src/lib/neurolink.ts (1)

7204-7214: ⚠️ Potential issue | 🟠 Major

Fallback still treats sentinel-only retry streams as success.

This gate now correctly sends sentinel-only primary streams into handleStreamFallback(), but the fallback path still treats any yielded chunk as success. If the fallback provider also emits only the shared no-output sentinel, fallbackChunkCount becomes non-zero and the call returns an effectively empty stream instead of surfacing the failure or continuing orchestration. Please make the fallback path use the same “real output chunk” predicate as this gate.

Suggested direction
-      let fallbackChunkCount = 0;
+      let fallbackChunkCount = 0;
+      let fallbackRealOutputChunks = 0;
       for await (const fallbackChunk of fallbackResult.stream) {
         fallbackChunkCount++;
+        const isNoOutputSentinel =
+          fallbackChunk !== null &&
+          typeof fallbackChunk === "object" &&
+          "metadata" in fallbackChunk &&
+          (fallbackChunk as { metadata?: Record<string, unknown> }).metadata
+            ?.noOutput === true;
+        const hasTextContent =
+          fallbackChunk &&
+          "content" in fallbackChunk &&
+          typeof fallbackChunk.content === "string" &&
+          fallbackChunk.content.length > 0;
+        const hasMediaPayload =
+          fallbackChunk !== null &&
+          typeof fallbackChunk === "object" &&
+          "type" in fallbackChunk &&
+          ((fallbackChunk as { type?: unknown }).type === "audio" ||
+            (fallbackChunk as { type?: unknown }).type === "image");
+        if (!isNoOutputSentinel && (hasTextContent || hasMediaPayload)) {
+          fallbackRealOutputChunks++;
+        }
         if (
           fallbackChunk &&
           "content" in fallbackChunk &&
           typeof fallbackChunk.content === "string"
         ) {
           appendContent(fallbackChunk.content);
           this.emitter.emit("response:chunk", fallbackChunk.content);
         }
         yield fallbackChunk;
       }

       if (
-        fallbackChunkCount === 0 &&
+        fallbackRealOutputChunks === 0 &&
         fallbackToolCalls.length === 0 &&
         fallbackToolResults.length === 0
       ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/neurolink.ts` around lines 7204 - 7214, The fallback path currently
counts any yielded chunk (including sentinel-only chunks) as success; update the
fallback consumer in handleStreamFallback so it uses the same real-output
predicate as the primary gate (i.e. only increment fallbackChunkCount for
non-sentinel/non-empty "real" chunks), referencing symbols like
realOutputChunks, handleStreamFallback, fallbackChunkCount, and the sentinel
detection logic used where realOutputChunks is computed; if the fallback emits
only sentinel chunks, treat it as a non-success (do not consider
fallbackChunkCount > 0) so the call can surface failure or continue
orchestration.
🧹 Nitpick comments (3)
src/lib/core/modules/StreamHandler.ts (2)

138-138: Yielded sentinel doesn't match the declared element type.

The generator is typed AsyncGenerator<{ content: string }> but yield sentinel (and the post-stream yield detected.sentinel) emits a NoOutputSentinel ({ content: "", metadata: {...} }). It compiles because excess properties are allowed when assigning from a variable, but downstream consumers can only see metadata after a cast — which defeats the centralized sentinel contract.

Consider widening the return type to AsyncGenerator<{ content: string } | NoOutputSentinel> (or to a discriminated union) so the metadata is visible at the type level and metadata.noOutput === true checks don't need any-casts.

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

In `@src/lib/core/modules/StreamHandler.ts` at line 138, The generator signature
in StreamHandler.ts currently returns AsyncGenerator<{ content: string }>, but
it yields the sentinel value (sentinel and detected.sentinel) of type
NoOutputSentinel; update the generator return type to AsyncGenerator<{ content:
string } | NoOutputSentinel> (or a discriminated union type that includes
NoOutputSentinel) so metadata is visible at the type level; adjust any places
that consume the generator to narrow on the discriminant (e.g.,
metadata.noOutput) instead of casting, and ensure the NoOutputSentinel type is
imported/used consistently where sentinel is created and yielded.

175-201: StreamHandler path can't capture upstream provider error like provider-specific paths do.

The provider stream-transformers in this PR (openAI, litellm, huggingFace, openRouter, anthropicBaseProvider, openaiCompatible) all capture the AI SDK's upstream error via streamText({ onError }) and pass it as the third underlyingError argument to buildNoOutputSentinel/detectPostStreamNoOutput, so the sentinel's providerError / modelResponseRaw carry the real cause (e.g. content_filter, provider crash) rather than the AI SDK's generic "No output generated" message.

StreamHandler.createTextStream does not accept an underlyingError parameter and calls both helpers without it. Any provider that goes through StreamHandler (rather than its own transformer) will silently lose this enrichment and emit the generic AI SDK message in telemetry. Consider extending the input contract to also accept an optional underlying-error getter (or value) and threading it through:

♻️ Suggested signature/threading
   createTextStream(result: {
     textStream: AsyncIterable<string>;
     finishReason?: Promise<unknown> | unknown;
     totalUsage?: Promise<unknown> | unknown;
+    /**
+     * Optional accessor for the upstream provider error captured via
+     * streamText's `onError` callback. Threaded into
+     * buildNoOutputSentinel / detectPostStreamNoOutput so the sentinel's
+     * providerError / modelResponseRaw carry the real cause.
+     */
+    getCapturedProviderError?: () => unknown;
   }): AsyncGenerator<{ content: string }> {
     const providerName = this.providerName;
     return (async function* () {
       ...
-          const sentinel = await buildNoOutputSentinel(error, result);
+          const sentinel = await buildNoOutputSentinel(
+            error,
+            result,
+            result.getCapturedProviderError?.(),
+          );
       ...
-        const detected = await detectPostStreamNoOutput(result);
+        const detected = await detectPostStreamNoOutput(
+          result,
+          result.getCapturedProviderError?.(),
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/core/modules/StreamHandler.ts` around lines 175 - 201,
createTextStream is losing provider-specific upstream errors because it doesn't
accept or forward an underlyingError to
buildNoOutputSentinel/detectPostStreamNoOutput; update
StreamHandler.createTextStream to accept an optional underlyingError (either an
Error or a getter function returning Error/Promise<Error>), thread that value
into the catch where buildNoOutputSentinel(...) is called and into the
post-stream detection call detectPostStreamNoOutput(result, underlyingError),
and ensure any stampNoOutputSpan calls continue to receive the sentinel
unchanged; also update all internal callers of createTextStream to pass the
provider's underlying error/getter so provider-specific errors flow into
providerError/modelResponseRaw.
test/continuous-test-suite-issue-06-no-output-context.ts (1)

162-213: Minor: abort timer is never cleared.

If the recipe completes before _abortAfterMs, the setTimeout handle (Line 172) is left to fire ac.abort() on an already-finished signal and keeps the event loop alive until it does. Not material for this CLI runner (it process.exits in main), but worth tracking the handle and clearing it in finally for cleanliness if you revisit this helper.

♻️ Optional cleanup
-  const abortAfter = (recipe.options as { _abortAfterMs?: number })
-    ._abortAfterMs;
-  if (abortAfter) {
-    setTimeout(() => ac.abort(), abortAfter);
-  }
+  const abortAfter = (recipe.options as { _abortAfterMs?: number })
+    ._abortAfterMs;
+  let abortTimer: ReturnType<typeof setTimeout> | undefined;
+  if (abortAfter) {
+    abortTimer = setTimeout(() => ac.abort(), abortAfter);
+  }
   try {
     // …
   } finally {
+    if (abortTimer) clearTimeout(abortTimer);
     await sdk.shutdown?.()?.catch(() => {});
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/continuous-test-suite-issue-06-no-output-context.ts` around lines 162 -
213, In tryRecipe, the abort timer created by setTimeout is never cleared;
capture the timeout handle when calling setTimeout (e.g., store it in a variable
like abortTimer) and in the finally block clear it (clearTimeout(abortTimer))
before calling sdk.shutdown to prevent the timer from keeping the event loop
alive; reference the AbortController ac, the abortAfter variable, and the
finally block around sdk.shutdown for where to add the clearTimeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/types/noOutputSentinel.ts`:
- Around line 7-27: Exported stream types use non-prefixed names; rename
NoOutputSentinel to StreamNoOutputSentinel and NoOutputSentinelResultLike to
StreamNoOutputSentinelResultLike, update all type declarations and export
identifiers in this file, and then update any imports/usages across the codebase
to reference the new names (e.g., replace occurrences of NoOutputSentinel and
NoOutputSentinelResultLike with StreamNoOutputSentinel and
StreamNoOutputSentinelResultLike).

In `@src/lib/utils/noOutputSentinel.ts`:
- Around line 71-84: The code reads (error as { cause?: unknown }).cause
unguarded which can throw if error is null/undefined; modify the cause
extraction so you only access error.cause when error is non-null (e.g. check
error != null && typeof error === "object" or use safe optional access) before
falling back to causeFromSource, and leave the modelResponseRaw logic unchanged
so it always slices a string (use String(cause) when present, otherwise use
`${messageSource.name}: ${messageSource.message}`) ensuring modelResponseRaw is
always populated as a string.
- Around line 38-42: The default usage object in noOutputSentinel.ts only
includes v4 keys (promptTokens, completionTokens, totalTokens), so downstream
consumers that expect v6 keys (inputTokens, outputTokens) will see zeros; update
the default `usage` value (the variable named usage in noOutputSentinel.ts) to
include both sets of keys — add inputTokens and outputTokens alongside
promptTokens and completionTokens (keep totalTokens) so callers like
amazonBedrock.ts and streamingClient.ts that read inputTokens/outputTokens ?? 0
get correct defaults; ensure buildNoOutputStatusMessage continues to work with
both shapes.

In `@test/continuous-test-suite-context.ts`:
- Around line 66-75: The current single VERTEX_TEST_MODEL defaults to
"gemini-2.5-pro" which causes Flash tests to run on Pro; split this into two
constants (e.g. VERTEX_PRO_TEST_MODEL and VERTEX_FLASH_TEST_MODEL), each reading
an explicit env override (like process.env.VERTEX_PRO_TEST_MODEL /
process.env.VERTEX_FLASH_TEST_MODEL) falling back to process.env.TEST_MODEL and
then to conservative defaults ("gemini-2.5-pro" for Pro and "gemini-2.5-flash"
for Flash), and replace uses of VERTEX_TEST_MODEL in Flash-specific tests with
VERTEX_FLASH_TEST_MODEL and Pro-specific tests with VERTEX_PRO_TEST_MODEL so
Flash tests no longer silently run on Pro.

In `@test/continuous-test-suite-issue-06-no-output-context.ts`:
- Around line 561-578: The existing doc comment for "Test 6.5 — INVESTIGATION"
is outdated and contradicts the test implementation (which now records FAIL via
calls in detectPostStreamNoOutput), so update the block comment to reflect that
Test 6.5 is now a regression gate that can FAIL CI; remove or rewrite the
"informational / recorded SKIP" wording and explicitly state the test now
asserts post-stream no-output detection and will record FAIL on regressions,
referencing the detectPostStreamNoOutput behavior and the fail-recording logic
used in this test.
- Around line 935-952: The FAIL branch references an undefined variable
usesRealContentChunks which will throw; update the template to use the existing
boolean usesRealOutputChunks (or otherwise define usesRealContentChunks) so the
record(...) call can safely log the two gate booleans; locate the conditional
that sets usesRealOutputChunks and incrementsForRealOnly and fix the template
literal in the record(...) call to reference usesRealOutputChunks and
incrementsForRealOnly alongside testName so failures are recorded instead of
causing a ReferenceError.

---

Outside diff comments:
In `@src/lib/neurolink.ts`:
- Around line 7204-7214: The fallback path currently counts any yielded chunk
(including sentinel-only chunks) as success; update the fallback consumer in
handleStreamFallback so it uses the same real-output predicate as the primary
gate (i.e. only increment fallbackChunkCount for non-sentinel/non-empty "real"
chunks), referencing symbols like realOutputChunks, handleStreamFallback,
fallbackChunkCount, and the sentinel detection logic used where realOutputChunks
is computed; if the fallback emits only sentinel chunks, treat it as a
non-success (do not consider fallbackChunkCount > 0) so the call can surface
failure or continue orchestration.

---

Nitpick comments:
In `@src/lib/core/modules/StreamHandler.ts`:
- Line 138: The generator signature in StreamHandler.ts currently returns
AsyncGenerator<{ content: string }>, but it yields the sentinel value (sentinel
and detected.sentinel) of type NoOutputSentinel; update the generator return
type to AsyncGenerator<{ content: string } | NoOutputSentinel> (or a
discriminated union type that includes NoOutputSentinel) so metadata is visible
at the type level; adjust any places that consume the generator to narrow on the
discriminant (e.g., metadata.noOutput) instead of casting, and ensure the
NoOutputSentinel type is imported/used consistently where sentinel is created
and yielded.
- Around line 175-201: createTextStream is losing provider-specific upstream
errors because it doesn't accept or forward an underlyingError to
buildNoOutputSentinel/detectPostStreamNoOutput; update
StreamHandler.createTextStream to accept an optional underlyingError (either an
Error or a getter function returning Error/Promise<Error>), thread that value
into the catch where buildNoOutputSentinel(...) is called and into the
post-stream detection call detectPostStreamNoOutput(result, underlyingError),
and ensure any stampNoOutputSpan calls continue to receive the sentinel
unchanged; also update all internal callers of createTextStream to pass the
provider's underlying error/getter so provider-specific errors flow into
providerError/modelResponseRaw.

In `@test/continuous-test-suite-issue-06-no-output-context.ts`:
- Around line 162-213: In tryRecipe, the abort timer created by setTimeout is
never cleared; capture the timeout handle when calling setTimeout (e.g., store
it in a variable like abortTimer) and in the finally block clear it
(clearTimeout(abortTimer)) before calling sdk.shutdown to prevent the timer from
keeping the event loop alive; reference the AbortController ac, the abortAfter
variable, and the finally block around sdk.shutdown for where to add the
clearTimeout.
🪄 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: 07f6ad88-51ca-4da0-99ab-11fc6c900a70

📥 Commits

Reviewing files that changed from the base of the PR and between a8b018e and 47bf52f.

📒 Files selected for processing (18)
  • src/lib/core/modules/StreamHandler.ts
  • src/lib/neurolink.ts
  • src/lib/providers/anthropicBaseProvider.ts
  • src/lib/providers/huggingFace.ts
  • src/lib/providers/litellm.ts
  • src/lib/providers/openAI.ts
  • src/lib/providers/openRouter.ts
  • src/lib/providers/openaiCompatible.ts
  • src/lib/services/server/ai/observability/instrumentation.ts
  • src/lib/types/index.ts
  • src/lib/types/noOutputSentinel.ts
  • src/lib/types/stream.ts
  • src/lib/utils/noOutputSentinel.ts
  • test/continuous-test-suite-autoresearch-live.ts
  • test/continuous-test-suite-context.ts
  • test/continuous-test-suite-dynamic.ts
  • test/continuous-test-suite-issue-06-no-output-context.ts
  • test/continuous-test-suite-proxy.ts

Comment thread src/lib/types/noOutputSentinel.ts Outdated
Comment thread src/lib/utils/noOutputSentinel.ts
Comment thread src/lib/utils/noOutputSentinel.ts
Comment thread test/continuous-test-suite-context.ts
Comment thread test/continuous-test-suite-issue-06-no-output-context.ts
Comment thread test/continuous-test-suite-issue-06-no-output-context.ts
…data + actually trigger the catch path the production bug needs

Curator P3-6: when the AI SDK throws NoOutputGeneratedError on a
zero-output stream, the sentinel chunk yielded `{noOutput: true,
errorType: "NoOutputGeneratedError"}` but no `finishReason`, no `usage`,
no `providerError`, no `modelResponseRaw` — telemetry consumers had no
way to differentiate between content-filter, stop-sequence pre-emption,
or mid-stream provider crash.

Round-1 fix added a `buildNoOutputSentinel` helper and wired it into the
catch block of every provider's `for await (chunk of result.textStream)`
iteration. Round-2 deep investigation found that catch block is dead
code in AI SDK v6.0.141: the SDK rejects the metadata promises
(`result.finishReason`, `result.totalUsage`, `result.steps`) when 0
steps are recorded, but it does NOT throw from textStream itself —
the iteration just completes silently with 0 chunks. Verified by
direct AI SDK probing across 9 different SSE/connection scenarios.

Round-2 fix:
 1. New `detectPostStreamNoOutput(result)` helper in noOutputSentinel.ts
    awaits `result.finishReason`, returns `{sentinel, error}` when it
    rejects with NoOutputGeneratedError, else null.
 2. All 7 wired sites (openAI, openaiCompatible, litellm, huggingFace,
    openRouter, anthropicBaseProvider, StreamHandler) now call the
    helper after the textStream loop completes with 0 chunks. The
    existing catch block stays for defensive coverage of the rare path
    where AI SDK does throw from textStream.
 3. LiteLLM preserves yield-then-throw (yields sentinel, then throws
    `detected.error`) so its existing fallback chain still fires.
 4. StreamHandler additionally stamps the active OTel span with
    `langfuse.status_message` + `neurolink.no_output.finish_reason` so
    Pipeline B observability surfaces the right status.

Test (`test/continuous-test-suite-issue-06-no-output-context.ts`):
 - 6.0  STATIC: shipped sentinel literal has all 6 enriched keys
 - 6.1  STATIC: every wired provider uses the helper (regression guard)
 - 6.2  RUNTIME: helper produces full sentinel with correct types
 - 6.3  RUNTIME: helper reads partial finishReason/usage from a
        result-like, falls back to defaults when promises reject
 - 6.4  RUNTIME: helper extracts `error.cause` into modelResponseRaw
 - 6.5  END-TO-END: a local HTTP server replays the production trigger
        (200 OK + connection-kill before any data). Real NeuroLink
        stream consumption finds the enriched sentinel chunk with all
        6 keys. This proves the fix actually fires for the bug Curator
        captured — round-1's catch block did not.
 - 6.x  BEST-EFFORT: alphabet-wide stop-sequence recipe per provider
        (skips when provider emits any content; expected)

Result: 12 PASS, 0 FAIL, 3 SKIP, exit=0.
@murdore

murdore commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

Round-N: addressed all 10 reviewer findings (bfa23c89)

Verified each against current code; fixed every one that needed it. 32/0/3 PASS on the issue-06 suite.

Type system

  1. Type-prefix rule (CLAUDE.md 🎉 Phase 1 MCP Foundation Complete - Universal AI Platform Achievement  #9) — Renamed NoOutputSentinel → StreamNoOutputSentinel, NoOutputSentinelResultLike → StreamNoOutputSentinelResultLike. Updated every import: src/lib/utils/noOutputSentinel.ts, src/lib/types/stream.ts, src/lib/core/modules/StreamHandler.ts, src/lib/core/baseProvider.ts.

Helper hardening

  1. Unguarded null-cause access — (error as { cause?: unknown }).cause would TypeError when error is null/undefined. Fixed via error !== null && typeof error === "object" guard before falling back through causeFromSource → causeFromError → name+message. modelResponseRaw is still always populated as a string.
  2. v6 usage default — Default usage object now includes both AI SDK v4 (promptTokens/completionTokens) and v6 (inputTokens/outputTokens) keys, plus totalTokens. Downstream consumers reading either shape see correct zeros instead of undefined. (Verified: test 6.2 now reports usage: {promptTokens:0, completionTokens:0, inputTokens:0, outputTokens:0, totalTokens:0}.)

NeuroLink wrapper

  1. Fallback gate — handleStreamFallback now distinguishes sentinel chunks from real output (text-with-content OR media payload), mirroring the primary stream wrapper. A fallback that yields only the NoOutputSentinel is now correctly treated as a non-success and surfaces "Fallback provider X also returned 0 real output chunks" instead of hiding behind a phantom-positive chunkCount > 0.

StreamHandler API

  1. Generator return type — createTextStream now returns AsyncGenerator<{ content: string } | StreamNoOutputSentinel> (was { content: string } only); BaseProvider.createTextStream shadow type updated to match. Sentinel metadata is visible at the type level.
  2. underlyingError forwarding — createTextStream accepts an optional getUnderlyingError?: () => unknown getter that threads the captured upstream error into both buildNoOutputSentinel (catch path) and detectPostStreamNoOutput (post-loop path). All five StreamHandler-based providers now wire this through:
    • anthropic, azureOpenai, googleAiStudio, mistral: added onError callback to streamText({...}) that captures event.error to a closure let capturedProviderError, then createTextStream(result, () => capturedProviderError).
    • googleVertex: extended the existing tracking object with capturedProviderError and updated buildAISDKStreamOptions's tracking parameter type accordingly; the existing onError now writes to it.

Test suite

  1. VERTEX_TEST_MODEL split — test/continuous-test-suite-context.ts now defines VERTEX_PRO_TEST_MODEL (defaults to gemini-2.5-pro) and VERTEX_FLASH_TEST_MODEL (defaults to gemini-2.5-flash). Each reads its own dedicated env override → TEST_MODEL → family default. Pro-named tests use the Pro constant; Flash-named tests use the Flash constant. No more silent Pro-runs in Flash tests.
  2. Test 6.5 doc-comment — Rewrote the outdated "INVESTIGATION / informational" wording. The block now describes Test 6.5 as an end-to-end regression gate that records FAIL on regressions and SKIP only on bind failures (e.g. EPERM in sandboxes).
  3. usesRealContentChunks ReferenceError — Fixed FAIL branch in test 6.11 that referenced usesRealContentChunks (didn't exist); now references the actual usesRealOutputChunks boolean. The FAIL message now logs both gate=... and exclusion=... correctly.
  4. Abort timer leak — tryRecipe now captures the setTimeout handle into abortTimer and clears it in finally before sdk.shutdown(). Recipes that complete before the abort fires no longer keep the event loop alive.

Result

Results: 32 passed, 0 failed, 3 skipped, exit=0

@coderabbitai full review

@github-actions

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

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== src/lib/types/noOutputSentinel.ts ==="
cat src/lib/types/noOutputSentinel.ts

echo ""
echo "=== src/lib/utils/noOutputSentinel.ts ==="
cat src/lib/utils/noOutputSentinel.ts

echo ""
echo "=== src/lib/core/modules/StreamHandler.ts (relevant excerpt) ==="
grep -n "StreamNoOutputSentinel\|createTextStream\|getUnderlyingError\|detectPostStreamNoOutput\|buildNoOutputSentinel\|stampNoOutputSpan" src/lib/core/modules/StreamHandler.ts | head -60

echo ""
echo "=== src/lib/neurolink.ts - handleStreamFallback / realOutputChunks ==="
grep -n "realOutputChunks\|noOutput\|handleStreamFallback\|sentinel\|StreamNoOutputSentinel" src/lib/neurolink.ts | head -60

echo ""
echo "=== BaseProvider createTextStream signature ==="
grep -n "createTextStream\|StreamNoOutputSentinel\|AsyncGenerator" src/lib/core/baseProvider.ts | head -30

echo ""
echo "=== googleVertex capturedProviderError / tracking ==="
grep -n "capturedProviderError\|tracking\|buildAISDKStreamOptions\|onError" src/lib/providers/googleVertex.ts | head -30

echo ""
echo "=== anthropic/azureOpenai/googleAiStudio/mistral onError + capturedProviderError ==="
for f in src/lib/providers/anthropic.ts src/lib/providers/azureOpenai.ts src/lib/providers/googleAiStudio.ts src/lib/providers/mistral.ts; do
  echo "--- $f ---"
  grep -n "capturedProviderError\|onError\|createTextStream" "$f" | head -15
done

echo ""
echo "=== test/continuous-test-suite-context.ts VERTEX_PRO/FLASH ==="
grep -n "VERTEX_PRO_TEST_MODEL\|VERTEX_FLASH_TEST_MODEL\|VERTEX_TEST_MODEL\|gemini-2.5" test/continuous-test-suite-context.ts | head -20

echo ""
echo "=== test/continuous-test-suite-issue-06: usesRealOutputChunks, tryRecipe abortTimer ==="
grep -n "usesRealOutputChunks\|usesRealContentChunks\|abortTimer\|clearTimeout\|tryRecipe" test/continuous-test-suite-issue-06-no-output-context.ts | head -30

Length of output: 17058


@murdore All 10 findings verified against the code. Here's what the inspection confirmed:

# Finding Status
1 Type-prefix rename (StreamNoOutputSentinel / StreamNoOutputSentinelResultLike) ✅ All import sites updated
2 Null-cause guard ✅ error !== null && typeof error === "object" guards causeFromError; messageSource always an Error so causeFromSource cast is safe
3 v6 usage default ✅ Zero object includes promptTokens, completionTokens, inputTokens, outputTokens, totalTokens
4 VERTEX_TEST_MODEL split ✅ VERTEX_PRO_TEST_MODEL / VERTEX_FLASH_TEST_MODEL each have their own env override → TEST_MODEL → family default
5 Test 6.5 doc-comment ✅ (author-confirmed)
6 usesRealContentChunks ReferenceError ✅ All 5 call sites in the test file correctly use usesRealOutputChunks
7 Fallback gate ✅ handleStreamFallback excludes sentinel via ?.noOutput === true and surfaces "0 real output chunks" message
8 Generator return type ✅ AsyncGenerator<{ content: string } | StreamNoOutputSentinel> in both StreamHandler and BaseProvider shadow
9 underlyingError forwarding ✅ All 5 providers wired: anthropic, azureOpenai, googleAiStudio, mistral via closure + onError; googleVertex via extended tracking object
10 Abort timer leak ✅ abortTimer captured, cleared in finally before sdk.shutdown()

One very minor observation: StreamNoOutputSentinel.metadata.modelResponseRaw is typed as string | undefined, yet buildNoOutputSentinel unconditionally produces a string (fallback to ${name}: ${message}). The type could be tightened to string for accuracy, but this is purely cosmetic — it will never actually be undefined at runtime. Happy to leave it as defensive typing if preferred.

Triggering the full review now.


🧠 Learnings used
Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-17T04:30:25.521Z
Learning: In neurolink (src/lib/neurolink.ts), the `pipelineAHandled: true` flag on `generation:end` events is set ONLY by `finalizeGenerateRequestResult` (the SDK-level wrapper). Native providers (amazonBedrock.ts, ollama.ts, googleAiStudio.ts, googleVertex.ts) emit their own `generation:end` events WITHOUT this flag. The `initializeMetricsListeners()` early-return guard `if (data.pipelineAHandled) return` correctly prevents duplicates: it skips the SDK-level event while allowing native provider events through to Pipeline B.

Learnt from: murdore
Repo: juspay/neurolink PR: 0
File: :0-0
Timestamp: 2026-04-26T06:27:32.928Z
Learning: In juspay/neurolink PR `#989`, the `generation:end` dedup for `sdk.stream()` uses a per-call options-based context: `enhancedOptions._streamDedupContext` (type `StreamGenerationEndContext = { providerEmitted: boolean }`) is created fresh per `sdk.stream()` invocation. Native providers (googleVertex.ts, googleAiStudio.ts) call `markStreamProviderEmittedGenerationEnd(options)` before their emit; the `runStandardStreamRequest` finally block reads `dedupContext.providerEmitted` and skips the orchestration emit when already true. amazonBedrock.ts and ollama.ts have independent stream paths that never enter `runStandardStreamRequest` so they do not need instrumentation. The orchestration emit carries `pipelineAHandled: true` while native provider emits do not.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@murdore
murdore merged commit 6854af1 into release Apr 27, 2026
16 checks passed
@murdore
murdore deleted the fix/curator-issue-06-no-output-context branch April 27, 2026 04:09
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 9.59.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — bfa23c89 Deployed Apr 27, 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.

3 participants