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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 176 additions & 10 deletions src/lib/core/baseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ import { createTimeoutController, TimeoutError } from "../utils/timeout.js";
import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
import { TTSProcessor } from "../utils/ttsProcessor.js";
import { GenerationHandler } from "./modules/GenerationHandler.js";
import { GenerationHandler } from "./modules/GenerationHandler.js";import {
extractAndParseJSON,
validateAgainstSchema,
attemptRepair,
} from "../utils/structuredOutput.js";

// Import modules for composition
import { MessageBuilder } from "./modules/MessageBuilder.js";
import { StreamHandler } from "./modules/StreamHandler.js";
import { TelemetryHandler } from "./modules/TelemetryHandler.js";
import { addToolContextToMessages, Utilities } from "./modules/Utilities.js";
import { ToolsManager } from "./modules/ToolsManager.js";
import { Utilities } from "./modules/Utilities.js";
import { shouldUseStructuredOutput } from "../utils/messageBuilder.js";
import type { GenerationResult } from "$lib/types/generateTypes.js";

/**
* Abstract base class for all AI providers
Expand Down Expand Up @@ -449,28 +456,187 @@ export abstract class BaseProvider implements AIProvider {
}

/**
* Execute the generation with AI SDK - delegated to GenerationHandler
* Execute the generation with AI SDK
* Routes to standard or structured output generation based on schema presence
*/
private async executeGeneration(
model: LanguageModelV1,
messages: CoreMessage[],
tools: Record<string, Tool>,
options: TextGenerationOptions,
): Promise<Awaited<ReturnType<typeof generateText>>> {
return this.generationHandler.executeGeneration(
): Promise<GenerationResult> {
// Execute initial generation (Phase 1 for structured output, final for standard)
const initialGenerationResult =
await this.generationHandler.executeGeneration(
model,
messages,
tools,
options,
);

// If no valid schema, return standard generation result
if (
!shouldUseStructuredOutput({
schema: options.schema,
output: options.output,
})
) {
return { ...initialGenerationResult, structuredOutputAchieved: false };
}

// Schema is valid - proceed with structured output orchestration (Phases 1-3)
return this.executeStructuredOutputGeneration(
model,
messages,
tools,
initialGenerationResult,
options.schema as ZodUnknownSchema,
options,
);
}

/**
* Execute 3-phase structured output orchestration
*
* Phase 1: Extract & validate JSON from initial generation (85-90% success, 1x cost)
* Phase 2: Repair if feasible (60-80% success, 1.2x cost)
* Phase 3: generateObject guaranteed (100% success, 2x cost)
*
* Average cost: ~1.15x baseline (vs 2x naive approach)
*
* @param model - Language model instance
* @param messages - Original messages from Phase 1 to preserve conversation context
* @param initialGenerationResult - Result from Phase 1 generateText call
* @param schema - Zod schema for validation
* @returns Validated structured output with success indicator
*/
private async executeStructuredOutputGeneration(
model: LanguageModelV1,
messages: CoreMessage[],
initialGenerationResult: Awaited<ReturnType<typeof generateText>>,
schema: ZodUnknownSchema,
options: TextGenerationOptions,
): Promise<GenerationResult> {
// ===== PHASE 1: Try extracting & validating JSON from initial generation =====
logger.debug(
`[BaseProvider] Phase 1: Attempting JSON extraction from initial generation`,
{
provider: this.providerName,
model: this.modelName,
},
);

// Extract JSON (handles markdown fences, brace-balanced, etc.)
const parsed = extractAndParseJSON(initialGenerationResult.text);

// Validate against schema
const validation = validateAgainstSchema(parsed, schema);

if (validation.success) {
logger.info(
`[BaseProvider] Phase 1: Success - JSON extracted and validated`,
{
provider: this.providerName,
model: this.modelName,
},
);

return {
...initialGenerationResult,
text: JSON.stringify(parsed),
structuredOutputAchieved: true,
};
}

// ===== PHASE 2: Try repair if validation failed =====
logger.debug(`[BaseProvider] Phase 2: Attempting repair`, {
provider: this.providerName,
model: this.modelName,
errorCount: validation.errors.length,
errors: validation.errors,
});

const enhancedMessages = addToolContextToMessages(
messages,
initialGenerationResult.toolCalls || [],
initialGenerationResult.toolResults || [],
);

const repairResult = await attemptRepair(
model,
parsed,
schema,
options,
validation.errors,
enhancedMessages,
);
Comment on lines +512 to +571

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

Phase 2 repair should force disableTools: true (avoid unintended tool side effects during “repair”).

attemptRepair(..., options, ...) gets the same options as the original generation. If tools are enabled, the “repair” pass can invoke tools again (non-deterministic, extra cost, possible side effects), which defeats the goal of a controlled schema-repair step.

Proposed diff
-    const repairResult = await attemptRepair(
+    const repairResult = await attemptRepair(
       model,
       parsed,
       schema,
-      options,
+      { ...options, disableTools: true, temperature: options.temperature ?? 0 },
       validation.errors,
       enhancedMessages,
     );
🤖 Prompt for AI Agents
In @src/lib/core/baseProvider.ts around lines 468 - 527, The repair pass is
reusing the original TextGenerationOptions so tools may be invoked again; create
a new options object for the repair step that sets disableTools: true without
mutating the original (e.g., const repairOptions = { ...options, disableTools:
true }) and pass repairOptions into attemptRepair(...) instead of options to
ensure the Phase 2 repair cannot trigger tools.


if (repairResult.success && repairResult.json) {
logger.info(`[BaseProvider] Phase 2: Success - JSON repaired`, {
provider: this.providerName,
model: this.modelName,
});

return {
...initialGenerationResult,
text: JSON.stringify(repairResult.json),
structuredOutputAchieved: true,
};
}

logger.debug(`[BaseProvider] Phase 2: Repair failed`, {
provider: this.providerName,
model: this.modelName,
});

// ===== PHASE 3: With generateObject =====
logger.info(
`[BaseProvider] Phase 3: Using generateObject for guaranteed success`,
{
provider: this.providerName,
model: this.modelName,
},
);

// Use original messages to preserve full conversation context
// Tool calls and results from Phase 1 are passed separately and will be added by executeObjectGeneration
try {
const phase3Result = await this.generationHandler.executeObjectGeneration(
model,
messages,
schema,
options,
initialGenerationResult.toolCalls,
initialGenerationResult.toolResults,
);

// Convert executeObjectGeneration result to generateText format
return {
...initialGenerationResult,
text: JSON.stringify(phase3Result.object),
finishReason: phase3Result.finishReason,
usage: phase3Result.usage,
warnings: phase3Result.warnings,
structuredOutputAchieved: true,
};
} catch (error) {
logger.error(`[BaseProvider] Phase 3: generateObject failed`, { error });
return repairResult.json
? {
...initialGenerationResult,
text: JSON.stringify(repairResult.json),
structuredOutputAchieved: false,
}
: {
...initialGenerationResult,
structuredOutputAchieved: false,
};
}
Comment on lines +592 to +633

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:

rg -n --type=ts "withTimeout\b|executeWithTimeout\b|AbortSignal|timeout:" src/lib/core src/lib/utils

Repository: juspay/neurolink

Length of output: 2917


🏁 Script executed:

# Check the Phase 1 and Phase 2 implementations in baseProvider.ts around the Phase 3 section
sed -n '480,590p' src/lib/core/baseProvider.ts | head -200

Repository: juspay/neurolink

Length of output: 3240


🏁 Script executed:

# Find executeObjectGeneration implementation
rg -n "executeObjectGeneration" src/lib/core/baseProvider.ts -B 2 -A 15

Repository: juspay/neurolink

Length of output: 1238


🏁 Script executed:

# Check how timeout/options are handled in generationHandler
rg -n "class.*GenerationHandler\|executeObjectGeneration\|executeTextGeneration" src/lib/core -A 5

Repository: juspay/neurolink

Length of output: 42


🏁 Script executed:

# Search for GenerationHandler class and executeObjectGeneration method
fd -t f "\.ts$" src/lib | xargs rg -l "executeObjectGeneration" | head -5

Repository: juspay/neurolink

Length of output: 130


🏁 Script executed:

# Find where generationHandler is instantiated or typed
rg -n "generationHandler" src/lib/core/baseProvider.ts | head -20

Repository: juspay/neurolink

Length of output: 583


🏁 Script executed:

# Look for GenerationHandler class definition
rg -n "class GenerationHandler\|interface GenerationHandler" src/lib -A 10

Repository: juspay/neurolink

Length of output: 42


🏁 Script executed:

# Read GenerationHandler.ts to find executeObjectGeneration implementation
cat -n src/lib/core/modules/GenerationHandler.ts | head -150

Repository: juspay/neurolink

Length of output: 6277


🏁 Script executed:

# Search for executeObjectGeneration method in GenerationHandler
rg -n "executeObjectGeneration" src/lib/core/modules/GenerationHandler.ts -A 20

Repository: juspay/neurolink

Length of output: 923


🏁 Script executed:

# Get the complete executeObjectGeneration method
sed -n '171,250p' src/lib/core/modules/GenerationHandler.ts

Repository: juspay/neurolink

Length of output: 2637


Wrap Phase 3 (generateObject) with timeout protection as required by coding guidelines.

Phase 3's executeObjectGeneration calls generateObject directly without timeout wrapping, violating the requirement to "Wrap async operations with withTimeout utility for timeout handling" in src/lib/**/*.ts files. The options.timeout parameter is passed but never utilized. Additionally, Phase 1 (executeGeneration) has the same issue—generateText also lacks explicit timeout protection. Consider wrapping both Phase 1 and Phase 3 calls with withTimeout(options.timeout) to ensure consistent timeout enforcement across all generation phases.

🤖 Prompt for AI Agents
In @src/lib/core/baseProvider.ts around lines 548 - 589, Phase 3 currently calls
generationHandler.executeObjectGeneration without timeout protection; wrap that
call with the withTimeout utility using options.timeout (and mirror the same
change for Phase 1 where executeGeneration/generateText is called) so async
generation calls are bounded; update the call sites in BaseProvider where
executeObjectGeneration and executeGeneration (generateText) are invoked to use
withTimeout(options.timeout, () =>
this.generationHandler.executeObjectGeneration(...)) (or the project’s
withTimeout signature) and ensure errors from timeout are handled in the
existing catch blocks to preserve repairResult fallback behavior.

}
Comment on lines +591 to +634

@coderabbitai coderabbitai Bot Jan 8, 2026 •

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

Phase 3: Fallback to generateObject with robust error handling.

The fallback correctly:

  • Uses generateObject for guaranteed schema compliance
  • Preserves tool context from Phase 1
  • Converts the result back to GenerationResult format

Error handling concern: When Phase 3 fails (lines 577-589), the fallback uses repairResult.json if available. However, this repairResult may have failed Phase 2 validation checks (including JSON format validation). Using potentially invalid JSON as a fallback introduces risk of malformed output. Return the original initialGenerationResult instead, which provides the unmodified Phase 1 text.

🤖 Prompt for AI Agents
In @src/lib/core/baseProvider.ts around lines 547 - 590, The catch block for
Phase 3 (around the call to generationHandler.executeObjectGeneration in
BaseProvider) currently returns repairResult.json on failure, which may itself
be invalid; instead, update the catch to return the original
initialGenerationResult (unmodified) with structuredOutputAchieved set to false
(and avoid injecting repairResult.json into text), so that on generateObject
failure you fall back to the safe Phase 1 output; reference
initialGenerationResult, repairResult, and
generationHandler.executeObjectGeneration when making this change.

@YaswanthKurapati24 YaswanthKurapati24 Jan 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If the generation fails in all the three times, submitting to the user the repairedJSON (though malformed, better than the original), along with the key structuredOutputAchieved: false, indicating the end user that the expected object generation failed. So that the end user can handle it in his desired way.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@YaswanthKurapati24, that makes sense! Your approach prioritizes providing the "best effort" output to the end user, even when all phases fail. Returning the repairResult.json (which at least attempted to address validation errors) along with structuredOutputAchieved: false gives the end user:

  1. The most refined attempt at structured output available
  2. A clear signal that validation didn't succeed
  3. Flexibility to decide how to handle the malformed output

This is a valid design choice that favors transparency and user control.

Optional suggestion: Consider adding a code comment in the Phase 3 catch block to document this intentional behavior for future maintainers:

} catch (error) {
  logger.error(`[BaseProvider] Phase 3: generateObject failed`, { error });
  // Intentional: Return repairResult.json (if available) even though it failed validation,
  // as it represents the "best effort" at structured output. The structuredOutputAchieved: false
  // flag signals to the end user that validation didn't succeed, allowing them to handle it appropriately.
  return repairResult.json
    ? {
        ...initialGenerationResult,
        text: JSON.stringify(repairResult.json),
        structuredOutputAchieved: false,
      }
    : {
        ...initialGenerationResult,
        structuredOutputAchieved: false,
      };
}

This would clarify the design intent for anyone reviewing the code later.


✏️ Learnings added
Learnt from: YaswanthKurapati24
Repo: juspay/neurolink PR: 767
File: src/lib/core/baseProvider.ts:547-590
Timestamp: 2026-01-09T07:07:04.204Z
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.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: juspay/neurolink PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-03T21:49:09.952Z
Learning: Applies to src/lib/providers/googleAiStudio.ts : Gemini models (AI Studio and Vertex) cannot use tools and JSON schema output simultaneously - design workflows to use either tools OR structured JSON output, not both


/**
* Log generation completion information - delegated to GenerationHandler
*/
private logGenerationComplete(
generateResult: Awaited<ReturnType<typeof generateText>>,
): void {
private logGenerationComplete(generateResult: GenerationResult): void {
this.generationHandler.logGenerationComplete(generateResult);
}

Expand Down Expand Up @@ -506,7 +672,7 @@ export abstract class BaseProvider implements AIProvider {
* Format the enhanced result - delegated to GenerationHandler
*/
private formatEnhancedResult(
generateResult: Awaited<ReturnType<typeof generateText>>,
generateResult: GenerationResult,
tools: Record<string, Tool>,
toolsUsed: string[],
toolExecutions: Array<{
Expand Down
Loading
Loading