Repository navigation
feat(object): add support for object generation in generate() #767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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, | ||
| ); | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: rg -n --type=ts "withTimeout\b|executeWithTimeout\b|AbortSignal|timeout:" src/lib/core src/lib/utilsRepository: 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 -200Repository: juspay/neurolink Length of output: 3240 🏁 Script executed: # Find executeObjectGeneration implementation
rg -n "executeObjectGeneration" src/lib/core/baseProvider.ts -B 2 -A 15Repository: 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 5Repository: 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 -5Repository: juspay/neurolink Length of output: 130 🏁 Script executed: # Find where generationHandler is instantiated or typed
rg -n "generationHandler" src/lib/core/baseProvider.ts | head -20Repository: juspay/neurolink Length of output: 583 🏁 Script executed: # Look for GenerationHandler class definition
rg -n "class GenerationHandler\|interface GenerationHandler" src/lib -A 10Repository: juspay/neurolink Length of output: 42 🏁 Script executed: # Read GenerationHandler.ts to find executeObjectGeneration implementation
cat -n src/lib/core/modules/GenerationHandler.ts | head -150Repository: juspay/neurolink Length of output: 6277 🏁 Script executed: # Search for executeObjectGeneration method in GenerationHandler
rg -n "executeObjectGeneration" src/lib/core/modules/GenerationHandler.ts -A 20Repository: juspay/neurolink Length of output: 923 🏁 Script executed: # Get the complete executeObjectGeneration method
sed -n '171,250p' src/lib/core/modules/GenerationHandler.tsRepository: juspay/neurolink Length of output: 2637 Wrap Phase 3 (generateObject) with timeout protection as required by coding guidelines. Phase 3's 🤖 Prompt for AI Agents |
||
| } | ||
|
Comment on lines
+591
to
+634
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Phase 3: Fallback to The fallback correctly:
Error handling concern: When Phase 3 fails (lines 577-589), the fallback uses 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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
🧠 Learnings used |
||
|
|
||
| /** | ||
| * Log generation completion information - delegated to GenerationHandler | ||
| */ | ||
| private logGenerationComplete( | ||
| generateResult: Awaited<ReturnType<typeof generateText>>, | ||
| ): void { | ||
| private logGenerationComplete(generateResult: GenerationResult): void { | ||
| this.generationHandler.logGenerationComplete(generateResult); | ||
| } | ||
|
|
||
|
|
@@ -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<{ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Phase 2 repair should force
disableTools: true(avoid unintended tool side effects during “repair”).attemptRepair(..., options, ...)gets the sameoptionsas 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
🤖 Prompt for AI Agents