Skip to content

feat: SmartHopper 2.0.0 - Google Gemini Provider, Batch API, Vision Input, and Breaking Changes#420

Open
marc-romu wants to merge 107 commits intodevfrom
feature/2.0.0-text2json
Open

feat: SmartHopper 2.0.0 - Google Gemini Provider, Batch API, Vision Input, and Breaking Changes#420
marc-romu wants to merge 107 commits intodevfrom
feature/2.0.0-text2json

Conversation

@marc-romu
Copy link
Copy Markdown
Member

@marc-romu marc-romu commented Apr 2, 2026

feat: SmartHopper 2.0.0 - Google Gemini Provider, Batch API, Vision Input, and Breaking Changes

Description

This PR introduces SmartHopper 2.0.0, a major release with significant new features, architectural improvements, and breaking changes. This is a large-scale refactor spanning 208 files with >22,000 insertions and 2,000 deletions.

Key Features Added

🚀 Google Gemini Provider (Full Integration)

  • Complete provider implementation with support for Gemini 3.1, 2.5, 2.0, and 1.5 models
  • Text generation with streaming support
  • Image generation capabilities
  • Structured outputs with JSON Schema support (Gemini subset)
  • Tool calling with function declarations
  • Extended thinking/reasoning with configurable thinking levels
  • Batch processing with priority support
  • Context caching infrastructure
  • x-goog-api-key authentication in centralized CallApi method

⚡ Mixed-Type Data Tree Support

  • Infrastructure for handling heterogeneous Grasshopper data types (GH_Boolean, GH_String, etc.)
  • New GHStructureConverter utility for converting typed structures to IGH_Goo
  • Enhanced DataTreeProcessor with IGH_Goo type gate for branch grouping
  • ProcessingResult<T> classes for returning output trees and messages
  • Migrated AIText2BooleanComponent and AIList2BooleanComponent to mixed-type pipelines

📊 Batch API Support (OpenAI, Anthropic, MistralAI, Gemini)

  • IAIBatchProvider interface implementation across three providers
  • Multi-item batch submission with custom ID tracking
  • Live progress counter during batch processing
  • Batch state persistence across file save/reopen
  • Per-item error surfacing to Grasshopper runtime messages
  • Configurable HTTP timeouts for large file operations
  • BatchTier parameter in AIRequestParameters (replaces service_tier extra)

🖼️ Vision Input Support

  • Image-to-text capabilities via img2text AI tool
  • AIInteractionImage with vision input methods and MimeType property
  • GH_ExtractedImage Goo type for Grasshopper integration
  • AIImgToTextComponent for standalone image description
  • AIFile2MdComponent with image modes: embed, describe, caption
  • Provider-specific vision encoding (OpenAI, Anthropic, MistralAI)

📄 File-to-Markdown Conversion

  • file2md AI tool supporting 12 formats: PDF, DOCX, XLSX, PPTX, HTML, CSV, JSON, XML, TXT, EML, EPUB, RTF
  • PDF layout intelligence with column detection, table detection, caption recognition
  • Image extraction from PDF, DOCX, and PPTX
  • File2MdComponent (non-AI) and AIFile2MdComponent (AI-powered)

🌐 Web-to-Markdown Conversion

  • web2md AI tool (replaces web_generic_page_read)
  • Specialized handlers for Wikipedia, GitHub, GitLab, Discourse, Stack Exchange
  • Readability scoring with boilerplate removal

⚙️ AI Settings Components

  • AISettingsComponent: Universal settings component for cross-provider parameters
  • AIExtraSettingsComponent: Dynamic inputs based on provider-specific extras
  • AIRequestParameters immutable record with fluent builder
  • GH_AIRequestParameters Grasshopper wrapper with backward-compatible string casting
  • Per-property resolution in all providers (input settings override global settings)

🛠️ JSON Tools & Components

  • text2json AI tool for structured JSON generation from prompts
  • AIText2JsonComponent for AI-powered JSON creation
  • JSON Helper Components: JsonSchemaComponent, JsonObjectComponent, JsonArrayComponent, JsonArray2TextListComponent, JsonObject2TextComponent, JsonGetValueComponent, JsonMergeComponent
  • Visual JSON Schema Builder: JsonSchemaPropComponent, JsonSchemaPropObjectComponent, JsonSchemaPropArrayComponent

Breaking Changes

⚠️ AI Tool Renames (Internal change - code migration required)

Old Name New Name
text_generate text2text
text_evaluate text2boolean
list_generate text2textlist
list_evaluate textlist2boolean
img_generate text2img
img_to_text img2text
web_to_md web2md
web_generic_page_read REMOVED (use web2md)

⚠️ Component Renames (Migration Required)

Old Class Name New Class Name
AITextGenerate AIText2TextComponent
AITextEvaluate AIText2BooleanComponent
AITextListGenerate AIText2TextListComponent
AIListEvaluate AIList2BooleanComponent
AIImgGenerateComponent AIText2ImgComponent
AIImgToTextComponent AIImg2TextComponent
WebPageReadComponent REMOVED (use Web2MdComponent)

Testing Done

Comprehensive testing plan created in docs/Reviews/260402-PR-Testing-Plan.md covering 202 test cases across 11 feature areas.

🔴 P0 - Breaking Changes (15 tests)

  • TC-BREAK-01: Open existing .gh files with old components - verify they load without errors
  • TC-BREAK-02: Verify AITextGenerateAIText2TextComponent migration works
  • TC-BREAK-03: Verify AITextEvaluateAIText2BooleanComponent migration works
  • TC-BREAK-04: Verify AITextListGenerateAIText2TextListComponent migration works
  • TC-BREAK-05: Verify AIListEvaluateAIList2BooleanComponent migration works
  • TC-BREAK-06: Verify AIImgGenerateComponentAIText2ImgComponent migration works
  • TC-BREAK-07: Verify AIImgToTextComponentAIImg2TextComponent migration works
  • TC-BREAK-08: Verify WebPageReadComponent removal doesn't crash file load
  • TC-BREAK-09: Test AI tools are accessible by new names in chat/tool calls
  • TC-BREAK-10: Verify old tool names return appropriate errors (not silent failures)
  • TC-BREAK-11: Check component outputs maintain same structure after rename
  • TC-BREAK-12: Verify Grasshopper wire connections preserved after component rename
  • TC-BREAK-13: Verify service_tier=batch in existing files is silently ignored
  • TC-BREAK-14: Test that Batch input on AISettingsComponent works correctly
  • TC-BREAK-15: Verify migration path documented: reconnect Batch input instead of service_tier extra

🔴 P0 - Google Gemini Provider (19 tests)

  • TC-GEMINI-01: Install and enable Google Gemini provider on clean install
  • TC-GEMINI-02: Configure API key in settings - verify secure storage
  • TC-GEMINI-03: Test provider appears in provider list with correct icon
  • TC-GEMINI-06: Test Gemini 2.0 Flash - text generation
  • TC-GEMINI-07: Test Gemini 2.5 Pro - text generation with reasoning
  • TC-GEMINI-08: Test Gemini 3.1 - text generation
  • TC-GEMINI-09: Test image generation models (if available)
  • TC-GEMINI-10: Verify all models appear in model selection dropdown with correct capabilities
  • TC-GEMINI-11: Text generation with streaming
  • TC-GEMINI-12: Structured output (JSON Schema)
  • TC-GEMINI-13: Tool calling with function declarations
  • TC-GEMINI-14: Extended thinking/reasoning levels
  • TC-GEMINI-15: Context caching (verify infrastructure in place)
  • TC-GEMINI-16: Test with AIText2TextComponent
  • TC-GEMINI-17: Test with AIText2BooleanComponent
  • TC-GEMINI-18: Test with AIText2JsonComponent
  • TC-GEMINI-19: Verify batch processing works with Gemini

🔴 P0 - Mixed-Type Data Trees (17 tests)

  • TC-DATATREE-01: Convert GH_Structure<GH_String> to GH_Structure<IGH_Goo>
  • TC-DATATREE-02: Convert GH_Structure<GH_Boolean> to GH_Structure<IGH_Goo>
  • TC-DATATREE-03: Convert GH_Structure<GH_Integer> to GH_Structure<IGH_Goo>
  • TC-DATATREE-04: Convert GH_Structure<GH_Number> to GH_Structure<IGH_Goo>
  • TC-DATATREE-05: Handle mixed-type trees with multiple IGH_Goo types
  • TC-DATATREE-06: Test groupIdenticalBranches with IGH_Goo type gate
  • TC-DATATREE-07: Verify branch grouping works with heterogeneous data
  • TC-DATATREE-08: Test RunAsync<T> overload for heterogeneous output
  • TC-DATATREE-09: Test ExtractTypedTree<U> helper method
  • TC-DATATREE-10: AIText2BooleanComponent - mixed-type input tree with GH_Boolean fallback
  • TC-DATATREE-11: AIList2BooleanComponent - mixed-type input tree with GH_Boolean fallback
  • TC-DATATREE-12: Verify fallback values stored natively without string conversion
  • TC-DATATREE-13: Test ProcessingResult<IGH_Goo>.Outputs access
  • TC-DATATREE-14: Test ExtractTypedTree<GH_String>() from heterogeneous results
  • TC-DATATREE-15: Test RunProcessingAsync<GH_String> with tree broadcasting
  • TC-DATATREE-16: Verify ItemGraft path management consistency
  • TC-DATATREE-17: Test ComponentProcessingOptions property behavior

🔴 P0 - Batch API (26 tests)

  • TC-BATCH-01: Submit multi-request JSONL file to /v1/files
  • TC-BATCH-02: Create batch via /v1/batches
  • TC-BATCH-03: Poll batch status with live progress counter
  • TC-BATCH-04: Download output from /v1/files/{output_file_id}/content
  • TC-BATCH-05: Cancel batch via /v1/batches/{id}/cancel
  • TC-BATCH-06: Verify request_counts.completed updates progress
  • TC-BATCH-07: Submit multiple items via POST /v1/messages/batches
  • TC-BATCH-08: Poll processing_status on GET /v1/messages/batches/{id}
  • TC-BATCH-09: Download JSONL results from results_url
  • TC-BATCH-10: Cancel via POST /v1/messages/batches/{id}/cancel
  • TC-BATCH-11: Verify request_counts.succeeded updates progress
  • TC-BATCH-12: Submit inline batching via POST /v1/batch/jobs
  • TC-BATCH-13: Poll job status on GET /v1/batch/jobs/{id}
  • TC-BATCH-14: Download output from /v1/files/{output_file}/content
  • TC-BATCH-15: Cancel via POST /v1/batch/jobs/{id}/cancel
  • TC-BATCH-16: Verify succeeded_requests updates progress
  • TC-BATCH-17: Test AITextGenerate batch completion with OnBatchCompleted override
  • TC-BATCH-18: Verify ReconstructOutputTree<T> replaces sentinels correctly
  • TC-BATCH-19: Test batch state persistence across file save/reopen
  • TC-BATCH-20: Verify CustomIds serialization in Write() / Read()
  • TC-BATCH-21: Test immediate first poll when restoring batch state
  • TC-BATCH-22: Verify batch capability validation prevents unsupported providers
  • TC-BATCH-23: Test batch item errors surface as Grasshopper runtime messages
  • TC-BATCH-24: Verify AIInteractionError detection in provider Decode() methods
  • TC-BATCH-25: Test large file upload/download timeout (300s default)
  • TC-BATCH-26: Verify progress messages show Preparing X/X... during data collection

🟡 P1 - Vision Input (25 tests)

  • TC-VISION-01: Create vision input from base64 data
  • TC-VISION-02: Create vision input from file path
  • TC-VISION-03: Verify MimeType property is correctly set
  • TC-VISION-04: Test CreateVisionInput() method
  • TC-VISION-05: Test CreateVisionInputFromBase64() method
  • TC-VISION-06: Test AddImageInput() fluent method
  • TC-VISION-07: Test AddImageInputFromBase64() fluent method
  • TC-VISION-08: OpenAI - verify base64 data URI encoding (data:{mime};base64,{data})
  • TC-VISION-09: Anthropic - native image content blocks with base64
  • TC-VISION-10: MistralAI - OpenAI-compatible image_url content blocks
  • TC-VISION-11: Test image description with imageUrl parameter
  • TC-VISION-12: Test image description with imageBase64 + mimeType
  • TC-VISION-13: Test with optional prompt parameter
  • TC-VISION-14: Verify AICapability.Image2Text requirement enforcement
  • TC-VISION-15: AIImgToTextComponent - file path input
  • TC-VISION-16: AIImgToTextComponent - URL input
  • TC-VISION-17: AIImgToTextComponent - base64 input
  • TC-VISION-18: AIImgToTextComponent - GH_ExtractedImage input (regression test for MistralAI fix)
  • TC-VISION-19: AIFile2MdComponent - image mode embed
  • TC-VISION-20: AIFile2MdComponent - image mode describe
  • TC-VISION-21: AIFile2MdComponent - image mode caption
  • TC-VISION-22: Verify ScriptVariable() returns Bitmap
  • TC-VISION-23: Test cast to/from GH_String
  • TC-VISION-24: Test cast to/from Bitmap
  • TC-VISION-25: Verify full serialization in GH files

🟡 P1 - File-to-Markdown (32 tests)

  • TC-F2MD-01: PDF conversion with layout intelligence
  • TC-F2MD-02: DOCX conversion (headings, tables, lists)
  • TC-F2MD-03: XLSX conversion
  • TC-F2MD-04: PPTX conversion
  • TC-F2MD-05: HTML conversion
  • TC-F2MD-06: CSV conversion
  • TC-F2MD-07: JSON conversion
  • TC-F2MD-08: XML conversion
  • TC-F2MD-09: TXT conversion
  • TC-F2MD-10: EML conversion
  • TC-F2MD-11: EPUB conversion
  • TC-F2MD-12: RTF conversion
  • TC-F2MD-13: Column detection with RecursiveXYCut
  • TC-F2MD-14: Reading order preservation
  • TC-F2MD-15: Header/footer removal (12%/88% zones)
  • TC-F2MD-16: Heading detection
  • TC-F2MD-17: Table detection via whitespace-gap analysis
  • TC-F2MD-18: Inline text styling preservation (bold, italic)
  • TC-F2MD-19: Caption detection for tables and figures
  • TC-F2MD-20: PDF image extraction via PdfPig
  • TC-F2MD-21: DOCX image extraction from ImageParts
  • TC-F2MD-22: PPTX image extraction from SlidePart.ImageParts
  • TC-F2MD-23: PNG conversion with TryGetPng
  • TC-F2MD-24: JPEG magic-byte fallback
  • TC-F2MD-25: File2MdComponent - basic conversion
  • TC-F2MD-26: File2MdComponent - Images output with GH_ExtractedImage
  • TC-F2MD-27: AIFile2MdComponent - AI-powered conversion
  • TC-F2MD-28: AIFile2MdComponent - image modes (embed, describe, caption)
  • TC-F2MD-29: AIFile2MdComponent - batch context persistence across save/reload
  • TC-F2MD-30: Test describeImages parameter
  • TC-F2MD-31: Verify images array always returned in result
  • TC-F2MD-32: Test imageMode parameter (embed, describe, caption)

🟡 P1 - Web-to-Markdown (12 tests)

  • TC-W2MD-01: Wikipedia article conversion
  • TC-W2MD-02: GitHub file URL conversion (raw/plain/markdown)
  • TC-W2MD-03: GitLab file URL conversion
  • TC-W2MD-04: Discourse forum conversion
  • TC-W2MD-05: Stack Exchange question conversion
  • TC-W2MD-06: Generic page fallback with readability scoring
  • TC-W2MD-07: Content scoring by text density
  • TC-W2MD-08: Link density filtering
  • TC-W2MD-09: Boilerplate removal (nav, header, footer, ads)
  • TC-W2MD-10: Semantic container prioritization (article, main)
  • TC-W2MD-11: Web2MdComponent - basic URL conversion
  • TC-W2MD-12: Verify web_generic_page_read removal - use web2md instead

🟡 P1 - AI Settings Components (23 tests)

  • TC-SETTINGS-01: Assemble AIRequestParameters from inputs
  • TC-SETTINGS-02: Test Model input (string)
  • TC-SETTINGS-03: Test Temperature input (number)
  • TC-SETTINGS-04: Test Max Tokens input (integer)
  • TC-SETTINGS-05: Test Top P input (number)
  • TC-SETTINGS-06: Test Seed input (integer)
  • TC-SETTINGS-07: Test Extras JSON input
  • TC-SETTINGS-08: Verify output Settings (S) wire connects to AI components
  • TC-SETTINGS-09: Test Batch (B) boolean input
  • TC-SETTINGS-10: Dynamic inputs rebuild on provider change
  • TC-SETTINGS-11: Test with OpenAI provider extras
  • TC-SETTINGS-12: Test with Anthropic provider extras
  • TC-SETTINGS-13: Test with MistralAI provider extras
  • TC-SETTINGS-14: Test with DeepSeek provider extras
  • TC-SETTINGS-15: Test with OpenRouter provider extras
  • TC-SETTINGS-16: Verify wire connections preserved on provider change
  • TC-SETTINGS-17: Test immutable record behavior
  • TC-SETTINGS-18: Test AIRequestParametersBuilder fluent methods
  • TC-SETTINGS-19: Test WithBatchTier() / ClearBatchTier()
  • TC-SETTINGS-20: Test serialization in GH_AIRequestParameters
  • TC-SETTINGS-21: Backward compatibility - plain string model name
  • TC-SETTINGS-22: Settings flow through to provider Encode()
  • TC-SETTINGS-23: Per-property resolution priority (input > global settings)

🟡 P1 - JSON Tools (24 tests)

  • TC-JSON-01: Generate JSON from prompt with JSON Schema
  • TC-JSON-02: Test prompt parameter (required)
  • TC-JSON-03: Test instructions parameter (optional)
  • TC-JSON-04: Test jsonSchema parameter (required)
  • TC-JSON-05: Verify AICapability.TextInput | JsonOutput enforcement
  • TC-JSON-06: Validate output conforms to provided schema
  • TC-JSON-07: Grasshopper component in SmartHopper > JSON category
  • TC-JSON-08: Test Prompt input (tree)
  • TC-JSON-09: Test Instructions input (tree, optional)
  • TC-JSON-10: Test Schema input (tree)
  • TC-JSON-11: Verify JSON output (text tree)
  • TC-JSON-12: Test ItemGraft + GroupIdenticalBranches processing topology
  • TC-JSON-13: JsonSchemaComponent - build schema from properties
  • TC-JSON-14: JsonSchemaComponent - nested properties via dot-notation
  • TC-JSON-15: JsonObjectComponent - create JSON from Key+Value lists
  • TC-JSON-16: JsonArrayComponent - create JSON array from items
  • TC-JSON-17: JsonArray2TextListComponent - parse JSON array to GH text list
  • TC-JSON-18: JsonObject2TextComponent - serialize JSON to string
  • TC-JSON-19: JsonGetValueComponent - extract nested value by dot-notation
  • TC-JSON-20: JsonMergeComponent - merge multiple JSON objects
  • TC-JSON-21: JsonSchemaPropComponent - scalar property definition
  • TC-JSON-22: JsonSchemaPropObjectComponent - object property with sub-properties
  • TC-JSON-23: JsonSchemaPropArrayComponent - array property with items type
  • TC-JSON-24: Verify components wire together correctly

🟢 P2 - Provider Model Updates (11 tests)

  • TC-MODEL-01: GPT-5.4 series models available
  • TC-MODEL-02: GPT-5.4-mini model works
  • TC-MODEL-03: GPT-5.4-nano model works
  • TC-MODEL-04: gpt-image-1.5 for image generation
  • TC-MODEL-05: Versioned model aliases available
  • TC-MODEL-06: mistral-small-4-0-26-03 works
  • TC-MODEL-07: mistral-large-3-25-12 works
  • TC-MODEL-08: ministral models work
  • TC-MODEL-09: codestral-25-08 works
  • TC-MODEL-10: GPT-5.4 series available
  • TC-MODEL-11: Claude 4.6 series available

🟢 P2 - UI/UX (8 tests)

  • TC-UI-01: Live progress counter shows Processing batch (0/XX)...
  • TC-UI-02: Counter updates during polling (YY/XX)...
  • TC-UI-03: Preparing X/X... shown during data collection
  • TC-UI-04: Stale progress reset on new run
  • TC-UI-05: Terminal states show base message (not stale progress)
  • TC-UI-06: User messages appear correctly (no dedup issues)
  • TC-UI-07: Tool results inherit TurnId from ToolCall
  • TC-UI-08: Metrics aggregation per turn works

Migration Guide

SmartHopper 2.0.0-alpha will automatically install from Rhino Package Manager once released.

For Breaking Changes

  1. Open existing .gh files - Components should migrate automatically on load. You might need to remove and replace some components if you expirience issues.
  2. Replace removed components - WebPageReadComponentWeb2MdComponent

For New Features

  1. Enable Google Gemini - Add API key in SmartHopper settings
  2. Try Batch Processing - Use AISettingsComponent with Batch=true on compatible components and providers
  3. Vision Workflows - Connect images to AIImgToTextComponent or use in AIFile2MdComponent
  4. JSON Generation - Use AIText2JsonComponent with schema inputs

Checklist

  • This PR is focused on a single major release (all 2.0.0 features)
  • Version in Solution.props updated to 2.0.0-dev.260402
  • CHANGELOG.md has been updated with all changes under [Unreleased]
  • PR title follows Conventional Commits format
  • PR description follows Pull Request Description Template
  • Comprehensive testing plan created in docs/Reviews/260402-PR-Testing-Plan.md

Related Issues

  • Addresses multiple feature requests for batch processing, vision input, and JSON support
  • Implements Google Gemini provider (tracked internally)
  • Closes breaking change debt from inconsistent AI tool naming

Open in Devin Review

…xt2json tool

- Add `text2json` AI tool for generating structured JSON from prompts conforming to JSON Schema
- Add `AIText2JsonComponent` in `SmartHopper > JSON` category with Prompt, Instructions, and Schema inputs
- Add JSON utility components requiring no AI:
  - `JsonSchemaComponent`: builds JSON Schema from property definitions with dot-notation nesting support
  - `JsonObjectComponent`: creates JSON objects from key-value pairs
…mposition

- Add `JsonSchemaPropComponent` (`JsonSchemaProp`): builds scalar property definitions from Name, Type, and Description inputs
- Add `JsonSchemaPropObjectComponent` (`JsonSchemaPropObj`): builds object properties by prefixing sub-properties with dot-notation
- Add `JsonSchemaPropArrayComponent` (`JsonSchemaPropArr`): builds array properties with configurable Items Type using `array[itemsType]` encoding
- Update `JsonSchemaComponent`
…t reconstruction

- Add `OnBatchCompleted` override to decode batch results and reconstruct output trees
- Add batch submission calls in worker `DoWorkAsync` after processing all items
- Add reconstructed tree handling in worker `SetOutput` to use batch results when available
- Implement provider-based result decoding using `AIInteractionText` extraction
- Add batch support to `AIImg2TextComponent`, `AIText2JsonComponent`, `AIFile
…nent

When a batch job completes but individual items fail (e.g. OpenAI `BadRequest` due to `max_tokens` too large), the error was silently discarded and the component showed "Done".
…ns via img2text

- **AIFile2MdComponent**: File conversion and image extraction now run locally via `file2md` with `describeImages=false`. Each extracted image is then described via `CallAiToolAsync("img2text", ...)` which is batch-interceptable. `OnBatchCompleted` reassembles final markdown from locally-stored per-file context and batch image description results using `ImageSentinelContext` dictionary.
…re queuing

- Add `IsValid()` validation call on batch requests before adding to queue
- Surface validation warnings to component using `SurfaceMessagesFromReturn`
- Surface validation errors but allow queuing to proceed (non-blocking)
- Add debug logging for validation message counts
…ding images section

- Add `InsertImagePlaceholders` to inject `[image N]` placeholders into markdown during file2md conversion
- Add `ImageIndex` property to `ImageSentinelContext` for 1-based placeholder tracking
- Replace image section assembly with placeholder substitution in both batch and non-batch modes
- Add `_sentinelContextsInitialized` flag to prevent clearing sentinel contexts during batch polling re-runs
… days

- Update milestone management guide to reflect 15-day waiting period between beta and rc releases
- Update GitHub Actions default `days-lookback` parameter from 30 to 15 days
- Update promotion PR body template to reference 15-day period
- Update release workflow documentation to reflect 15-day requirements for release age and last closed issue
- Update release-promotion workflow comments and validation table to show 15-day minimum
…ng brace in AIFile2MdComponent

- Add missing closing brace in `AIFile2MdComponent.OnBatchCompleted` after markdown assembly
- Consolidate warning and error validation message handling into single loop in `AIStatefulAsyncComponentBase`
- Use `AddRuntimeMessage(severity, origin, message)` instead of passing message object directly
- Update debug logging to reflect combined warning/error count
…nd skip output during active batch

- Move AIReturnSnapshot and batch state clearing from BeforeSolveInstance to new OnEnteringProcessingState hook
- Remove metricsInitializedForRun flag in favor of state transition-based clearing
- Add OnEnteringProcessingState virtual method to StatefulComponentBase called during Processing state entry
- Skip SetMetricsOutput in OnSolveInstancePostSolve when batch submission is still active
… convention

- Rename `WebToMdComponent` to `Web2MdComponent` and update nickname from "WebToMd" to "Web2Md"
- Rename icon resource from `webtomd` to `Web2Md`
- Rename `WebToMdAsync` method to `Web2MdAsync` in web2md AI tool
- Update CHANGELOG.md and DEV.md references from WebToMdComponent to Web2MdComponent
- Update debug logging prefix from "WebToMd" to "Web2Md"
…alongside McNeel and Ladybug forum support

- Add generic `DiscourseSearch`, `DiscoursePostGet`, `DiscoursePostOpen`, `DiscoursePostDeconstruct` components
- Add generic `AIDiscoursePostSummarize` and `AIDiscourseTopicSummarize` components
…e-line format in DiscourseToolsBase

- Replace verbatim multi-line JSON schema strings with single-line interpolated strings
- Rename `baseUrlSchemaProperty` to `baseUrlProperty` for consistency
- Simplify schema formatting while maintaining identical structure and functionality
…ng with improved logging

- Add try-catch block around CallAiToolAsync to handle exceptions during batch item processing
- Add null check for toolResult with warning message when tool returns null
- Add error messages with batch item index when exceptions occur
- Update debug logging to use "batch item" terminology instead of "index" for consistency
- Add empty string outputs for failed batch items to maintain output alignment
…centralizing timeout configuration

- Add `CreateBatchHttpClient()` protected helper method to `AIProvider` base class with 5-minute default timeout (300 seconds)
- Refactor `OpenAIProvider`, `AnthropicProvider`, and `MistralAIProvider` to use centralized helper instead of default HttpClient
- Add timeout clamping (1-600 seconds) and debug logging for batch HTTP client creation
- Update CHANGELOG.md to document batch processing
…with configurable UI controls

- Add `HttpTimeoutSeconds` (default 120s) and `BatchHttpTimeoutSeconds` (default 300s) global settings in ProvidersSettingsPage under "Network Settings" section
- Add NumericStepper controls (1-600 seconds range) with descriptive labels and help text for both timeout types
- Update `AIProvider.CreateBatchHttpClient()` to read `BatchHttpTimeoutSeconds` from provider settings instead of hardcoded 300s default
…per-file storage with ordered image slots

- Replace `ImageSentinelContext` with `FileBatchContext` containing base markdown and ordered `ImageSlot` list
- Rename `_sentinelContexts` to `_fileContexts` and key by representative sentinel ID (first image) instead of per-image
- Add `ImageSlot` class to store per-image metadata (index, sentinel ID, mode, context, MIME type, base64)
…stead of per-provider settings

- Update `CreateBatchHttpClient()` to read `BatchHttpTimeoutSeconds` from global settings instead of provider-specific settings
- Update `CreateHttpClient()` to read `HttpTimeoutSeconds` from global settings instead of provider-specific settings
- Use `SmartHopperSettings.Instance.GetSetting("Global", ...)` for both timeout configurations
- Maintain same default values (120s for HTTP, 300s for batch
…nel tree output during active batch operations

- Add debug logging throughout File2Md and file2md batch processing to track tool execution flow
- Override `RestorePersistentOutputs()` in AIStatefulAsyncComponentBase to skip sentinel tree output during active batch submission
- Update AIFile2MdComponent to extract image descriptions from AIInteractionToolResult instead of AIInteractionText
…ription extraction across execute and batch paths

- Add `Write`/`Read` methods to serialize `_fileContexts` (base markdown + image slot metadata) so batch results survive Grasshopper file save/reload
- Add `_batchContextLost` flag to prevent `GatherInput` from resetting `_fileContextsInitialized` during active batch, fixing same-session context overwrite bug
- Update `OnBatchCompleted` to extract image descriptions from `AIInteractionText.Content` instead of
…s persistence after batch completion

- Add `HasActiveBatchSubmission` protected property to distinguish poll cycles from new runs in derived classes
- Call `SetMetricsOutput(null)` in `OnBatchCompleted` to persist aggregated metrics for `RestorePersistentOutputs` (since `OnSolveInstancePostSolve` is skipped during batch)
- Update `AIMetrics.CombineWith()` to skip overwriting Provider/Model with "Unknown" or empty values during
marc-romu added 24 commits April 3, 2026 15:48
- Replace ASCII encoding with UTF-8 BOM () in file headers across 5 test files
- Change HtmlReadabilityHelper.ExtractMainContent reflection binding from NonPublic to Public in 7 test methods
- Remove GH_ExtractedImage tests (204 lines) from ExtractedImageTests.cs, keeping only ExtractedImagePocoTests
- Remove GH_Structure tests (164 lines) from DataTreeProcessorMixedTypeTests.cs, keeping only ProcessingTopology and ProcessingOptions
…ation and GH_Path/GH_Structure test implementations

- Add GrasshopperTestComponents.md with 198 lines documenting test component pattern, structure, implementation guidelines, and migration from xUnit
- Document test component characteristics: septenary exposure, async worker pattern, success/messages outputs, comprehensive logging
- Add comparison table between xUnit tests and test components for Grasshopper-dependent testing
…maProp components

- Add validation to reject colons (:) in name and description fields for both JsonSchemaObjectComponent and JsonSchemaPropComponent
- Add error messages "Name cannot contain colons (:)." and "Description cannot contain colons (:)." when validation fails
- Insert validation checks after empty string validation and before name trimming in both components
…provider-specific tests and hardcoded test data approach

- Add 30 provider-specific test components across 6 providers (OpenAI, MistralAI, DeepSeek, Gemini, Anthropic, OpenRouter)
- Document 5 test cases per provider: Encode, Decode, Standard Call, Batch Call, Tools/Function Calling
- Clarify all test data is hardcoded internally - users only toggle Run=true
- Reduce core functionality tests from 25 to 20 (removed unsuitable tests requiring
- Change "Stack to describing" to "Stick to describing" in DefaultImageDescriptionPrompt constant
- Add gemini_icon.png to Resources folder
- Register gemini_icon in Resources.resx as embedded resource
- Update GeminiProvider.Icon property to return Properties.Resources.gemini_icon instead of null
… source files

- Remove trailing spaces from markdown files (DataTreeProcessingSchema.md, PR-Testing-Plan.md)
- Normalize file encoding from UTF-8 with BOM to UTF-8 in AIStatefulAsyncComponentBase.cs
- Add null-coalescing operator to Messages property initialization in AIBatchTypes.cs
…re and add OpenAI provider project

- Add SmartHopper.Providers.OpenAI project to solution with all build configurations
- Update Anthropic test components to use new AICall.Core namespace structure (Base, Interactions, Requests, Returns)
- Replace AIInteraction with specific interaction types (AIInteractionText, AIInteractionToolCall, AIInteractionToolResult)
- Change AIInteraction.Role to AIInteraction.Agent across all test components
- Replace
…iation patterns

- Add settings.png and settingsextra.png icon resources for AISettings and AIExtraSettings components
- Update AISettingsComponent and AIExtraSettingsComponent to use new icon resources instead of null
- Fix Discourse and Ladybug forum topic summarize components to use correct icons (discoursetopicsummarize, ladybugtopicsummarize)
- Refactor GeminiProvider to use AIProvider<GeminiProvider>.Instance singleton pattern,
…omponent icons

- Add jsonarray.png and jsonobj.png icon resources for JsonArray and JsonObject components
- Update JsonArrayComponent and JsonObjectComponent to use new icons instead of textgenerate
- Update forum component icons (Discourse, Ladybug, McNeel) for search, get, open, and summarize operations
- Add sealed modifier to GeminiProvider partial class declarations
- Add exception handling and debug logging to GeminiProvider
…sage metrics extraction, and improve image handling

- Add service_tier extra setting for per-request tier override (standard/flex/priority)
- Add batch_priority extra setting for batch request prioritization
- Refactor batch API to use correct Gemini format with InlinedRequest structure and Operation polling
- Add ExtractUsageMetadata method to capture promptTokenCount, candidatesTokenCount, and thoughtsTokenCount
…roviders and fix stopwatch cleanup

- Add structured AIReturn error handling in AIProvider.CallApi for HTTP failures instead of throwing exceptions
- Add context-specific error messages for common HTTP status codes (503, 429, 401/403, 408, 413, 500, 502, 504)
- Include provider-specific guidance (Flex tier for 503, retry delays for 429, API key checks for auth errors)
- Tag all HTTP errors with Provider origin for consistent UI
… text2json prompt

- Comment out colon validation in JsonSchemaObjectComponent and JsonSchemaPropComponent description fields
- Add instruction to text2json default prompt to not return a copy of the JSON Schema in the response
…lution logic

- Change AIRequestBase.TimeoutSeconds to nullable int? to allow null/empty values
- Update RequestTimeoutPolicy to resolve timeout from settings when null (reads "TimeoutSeconds" with fallback to "HttpTimeoutSeconds" for backward compatibility, uses 300s default)
- Add Timeout input parameter to AISettingsComponent (positioned before Extras)
- Add ConfigureRequestTimeout() helper method to AIStatefulAsyncComponentBase for centralized timeout configuration
- Remove duplicate timeout resolution logic from AI
…r credits

- Update Rhinoceros icon URL to direct icon link instead of search term
- Reorder attribution section to show icon credits before logo design thanks label
…imeSpan.FromSeconds

- Cast timeout calculation result to double in AIToolCall.ExecuteAsync to ensure correct TimeSpan.FromSeconds overload is used
… nullable TimeoutSeconds

- Add explicit double cast to TimeoutSeconds fallback expressions in all providers to ensure correct TimeSpan.FromSeconds overload
- Extract clamped timeout calculation to separate variable in AIToolCall.ExecuteAsync for clarity
- Update Gemini streaming to use .Value accessor for nullable TimeoutSeconds
- Handle nullable TimeoutSeconds with null-coalescing in AIToolCall timeout resolution
…ze JSON path error formatting

- Add bracket notation support (e.g., "results[0].name", "items[5]") to JsonGetValueComponent alongside existing dot notation
- Implement ParsePathSegments() method to parse mixed dot and bracket notation paths into PathSegment objects
- Add PathSegment class to represent property names and array indices in JSON paths
- Create JsonPathHelper utility class with FormatJsonPathError, FormatJsonPathWarning, For
…raction and minification

- Create JsonFormatHelper utility class in SmartHopper.Infrastructure.Utilities for consistent JSON formatting across all components
- Add core methods: JsonToString() (JToken/string to minified JSON), StringToJson() (string to JToken), IsValidJson() (validation with optional parsing)
- Implement automatic markdown code block extraction (```json, ```txt, ```text, ```) in string-based methods before processing
…sition in WebChatObserver

- Add comment explaining that turn completion marks exit from processing state and readiness for next user message
devin-ai-integration[bot]

This comment was marked as resolved.

…rs for all test provider components

- Update ComponentGuid for TestAnthropicBatchCallComponent, TestAnthropicDecodeComponent, TestAnthropicEncodeComponent, TestAnthropicStandardCallComponent, TestAnthropicToolsComponent
- Update ComponentGuid for TestGeminiDecodeComponent, TestGeminiEncodeComponent, TestGeminiFunctionCallingComponent, TestGeminiStandardCallComponent, TestGeminiVisionComponent
- Replace duplicate/placeholder GUIDs with properly
… requests for batch status and cancel operations

- Add SendBatchRequestAsync helper method for direct HTTP GET/POST requests to batch endpoints
- Replace Call/Decode pipeline usage in GetBatchStatusAsync with direct HTTP GET to avoid unnecessary response parsing through generateContent decoder
- Replace Call/Decode pipeline usage in CancelBatchAsync with direct HTTP POST for consistency
- Add explanatory comment that batch Operation
devin-ai-integration[bot]

This comment was marked as resolved.

…gs and register textlist2boolean tool in AIList2BooleanComponent

- Remove redundant bool.TryParse attempt on non-parseable strings in both AIText2BooleanComponent and AIList2BooleanComponent
- Directly treat non-parseable strings as fallback cases (null result, usedFallback=true) instead of attempting secondary parse
- Change UsingAiTools from virtual to override in AIList2BooleanComponent and register "textlist2boolean" tool
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 18 new potential issues.

Open in Devin Review

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Discourse tools consolidation — 6 classes replaced by 3 via DiscourseToolsBase refactor

The PR consolidates separate post/topic tool classes (mcneelpost2text, mcneeltopic2text, ladybugpost2text, ladybugtopic2text, discoursepost2text, discoursetopic2text) into 3 classes (discourse_mcneel_tools, discourse_ladybug_tools, discourse_tools) that inherit from the refactored DiscourseToolsBase. The base class now uses PresetBaseUrl (nullable) instead of abstract BaseUrl, and the generic discourse_tools variant requires base_url as a parameter. The old classes that filtered tools (e.g., only post-related or topic-related) are gone — each new class now exposes ALL tools (search, get_post, get_topic, summarize_post, summarize_topic). This means McNeel and Ladybug forum tool sets doubled in size. The GetTools() override that filtered is removed. Verified tool names remain backward-compatible (mcneel_forum_search, ladybug_forum_post_get, etc.).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +386 to +399
// Forward it so ReconstructOutputTree can replace it after the batch completes.
var resultValue = toolResult["result"]?.ToString();
if (resultValue != null && resultValue.StartsWith("##SH_BATCH:", StringComparison.Ordinal))
{
return result;
outputs["Result"].Add(new GH_String(resultValue));
continue;
}

// Non-batch: get the result (could be boolean string or fallback value)
// The result from the tool is already processed
outputs["Result"].Add(new GH_String(resultValue ?? string.Empty));
}

return null;
return outputs;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 "Used Fallback" output is always false in non-batch mode when fallback IS used

In AIText2BooleanComponent and AIList2BooleanComponent, when the AI returns an unparseable response and the text2boolean/textlist2boolean tool applies the fallback value, the component's ProcessData only extracts toolResult["result"] as a string (which is now the boolean fallback value, e.g., "True"). Later, ConvertStringTreeToBoolean sees this valid boolean string and marks usedFallback = false. The tool DID set toolResult["usedFallback"] = true, but the component never reads that field. So the "Used Fallback" output always reports false even when the fallback was actually used, making it impossible for users to distinguish "AI said true" from "AI returned gibberish, fallback was applied".

Trace through the code
  1. AI returns unparseable text (e.g., "I think yes")
  2. Tool at text2boolean.cs:186-206: sets result = fallback.Value, usedFallback = true
  3. Component ProcessData at AIText2BooleanComponent.cs:388: resultValue = toolResult["result"]?.ToString()"True"
  4. ConvertStringTreeToBoolean at line 288: bool.TryParse("True") succeeds → usedFallbackBranch.Add(new GH_Boolean(false))
Prompt for agents
The component's ProcessData method only extracts toolResult["result"] but ignores toolResult["usedFallback"]. The outputs dictionary should carry both values so ConvertStringTreeToBoolean (or a replacement) can correctly report whether the fallback was used.

Approach: In ProcessData, extract both "result" and "usedFallback" from the tool result. Instead of storing a single "Result" string that gets re-parsed, store a tuple or use two separate output lists (one for the boolean result, one for the usedFallback flag). Then ConvertStringTreeToBoolean can use the tool's usedFallback directly instead of re-inferring it from string parseability.

The same fix is needed in AIList2BooleanComponent.cs ProcessData (around line 395-405).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +146 to +170
private static (GH_Boolean value, bool usedFallback) ParseBooleanWithFallback(
JObject resultBody,
System.Func<JObject, System.Collections.Generic.List<IAIInteraction>> decode)
{
if (resultBody == null)
{
return (null, true);
}

var interactions = decode(resultBody);
var lastText = interactions
?.OfType<AIInteractionText>()
.LastOrDefault(i => i.Agent == AIAgent.Assistant);

if (lastText == null)
{
return (null, true);
}

if (bool.TryParse(lastText.Content?.Trim(), out bool value))
{
return (new GH_Boolean(value), false);
}

return (null, true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Batch mode never applies fallback value for boolean components — result is null instead of fallback

In AIText2BooleanComponent.OnBatchCompleted and AIList2BooleanComponent.OnBatchCompleted, the batch path uses ParseBooleanWithFallback which decodes the raw provider response and attempts bool.TryParse. If the AI response isn't a clean boolean, it returns (null, true) — the fallback value from the component's "Fallback" input is never consulted. In non-batch mode, the text2boolean tool applies the fallback inside its execute function (text2boolean.cs:186-206). But in batch mode, the tool's execute function doesn't run — only BuildEvaluateRequest runs, and the raw provider response is decoded directly in OnBatchCompleted. The user's fallback value is silently lost, resulting in null output items instead of the configured fallback boolean.

Prompt for agents
ParseBooleanWithFallback in AIText2BooleanComponent (and the identical copy in AIList2BooleanComponent) needs access to the component's fallback input value so it can apply it when bool.TryParse fails. Currently ParseBooleanWithFallback is a static method that only receives the result body and decode function.

Approach: Either (1) make ParseBooleanWithFallback accept a bool? fallbackValue parameter, and when parsing fails, return (new GH_Boolean(fallbackValue.Value), true) instead of (null, true), or (2) store the fallback value in a component-level field during GatherInput and access it from OnBatchCompleted.

The same fix is needed in AIList2BooleanComponent.cs (around lines 147-170).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +108 to +139
this.ProcessBatchResults<GH_Boolean>(
"Result",
sentinel,
results,
(customId, resultBody) =>
{
// Decode and parse ONCE, cache both values
var (value, usedFallback) = ParseBooleanWithFallback(resultBody, provider.Decode);
this._batchParseCache[customId] = (value, usedFallback);
return value;
},
messages);

// Process Used Fallback output using cached values (no re-parse!)
// Use the same sentinel as Result since both trees have identical structure
this.ProcessBatchResults<GH_Boolean>(
"Used Fallback",
sentinel,
results,
(customId, resultBody) =>
{
// Retrieve from cache - no parsing needed
if (this._batchParseCache.TryGetValue(customId, out var cached))
{
return new GH_Boolean(cached.usedFallback);
}
return null;
},
messages);

// Clear cache
this._batchParseCache = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Double ProcessBatchResults causes every batch result to be provider-Decoded 3 times

In AIText2BooleanComponent.OnBatchCompleted (lines 108-141) and AIList2BooleanComponent.OnBatchCompleted, ProcessBatchResults is called twice — once for "Result" and once for "Used Fallback". Each ProcessBatchResults call internally calls provider.Decode(resultBody) at AIStatefulAsyncComponentBase.cs:1571 for every sentinel. Additionally, the first call's user decode lambda (ParseBooleanWithFallback) calls provider.Decode again. This means each batch result body is decoded 3 times total (2× in the first call + 1× in the second call), tripling the decode work. The second call also overwrites _persistedMetrics and AIReturnSnapshot set by the first call.

Prompt for agents
Instead of calling ProcessBatchResults twice, call it once for the primary output ("Result") and use TransformOutputs or manual iteration to populate the "Used Fallback" tree in the same pass. This avoids redundant provider.Decode calls and the double FinishResults/SetMetricsOutput emissions.

Alternatively, the decode cache pattern could be extended to also cache the decoded interactions so the second ProcessBatchResults call skips the internal provider.Decode step. But the cleanest fix is a single ProcessBatchResults call that populates both outputs.

The same pattern exists in AIList2BooleanComponent.OnBatchCompleted (lines ~110-144).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 108 to 109
this.Surfaceable = surfaceable;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: AIRuntimeMessage constructor breaking change — all existing callers must add AIMessageCode parameter

The 3-parameter constructor AIRuntimeMessage(severity, origin, message) was removed (see AIRuntimeMessage.cs diff). All call sites now use the 4-parameter version with an explicit AIMessageCode. This is visible in the diff where every new AIRuntimeMessage(...) call in validation, returns, and policy code was updated to include the code parameter. The AddRuntimeMessage(severity, origin, text) convenience method on AIReturn (AIReturn.cs:397) still exists and passes AIMessageCode.Unknown internally. Any external consumers or plugins that used the removed 3-parameter constructor will fail to compile. Since this is an infrastructure library, this is a deliberate API cleanup rather than a bug.

(Refers to lines 102-109)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +108 to +139
this.ProcessBatchResults<GH_Boolean>(
"Result",
sentinel,
results,
(customId, resultBody) =>
{
// Decode and parse ONCE, cache both values
var (value, usedFallback) = ParseBooleanWithFallback(resultBody, provider.Decode);
this._batchParseCache[customId] = (value, usedFallback);
return value;
},
messages);

// Process Used Fallback output using cached values (no re-parse!)
// Use the same sentinel as Result since both trees have identical structure
this.ProcessBatchResults<GH_Boolean>(
"Used Fallback",
sentinel,
results,
(customId, resultBody) =>
{
// Retrieve from cache - no parsing needed
if (this._batchParseCache.TryGetValue(customId, out var cached))
{
return new GH_Boolean(cached.usedFallback);
}
return null;
},
messages);

// Clear cache
this._batchParseCache = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 ProcessBatchResults called twice for AIText2Boolean/AIList2Boolean with same sentinel tree

In AIText2BooleanComponent.OnBatchCompleted (lines 107-139), ProcessBatchResults is called twice on the same sentinel tree: once for 'Result' and once for 'Used Fallback'. Each call to ProcessBatchResults calls FinishResults internally, which calls SetPersistentOutput and SetMetricsOutput. The second call to ProcessBatchResults will overwrite the metrics persisted by the first call and call FinishResults again for the 'Used Fallback' output. This works because FinishResults is additive for SetPersistentOutput (it doesn't clear previous outputs), but the metrics are emitted twice. The _batchParseCache pattern avoids double-parsing, which is good, but the double FinishResults call is architecturally unusual compared to other components that use a single FinishResults with additionalOutputs.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread SmartHopper.sln
Comment on lines +20 to +22
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Providers.OpenAI", "src\SmartHopper.Providers.OpenAI\SmartHopper.Providers.OpenAI.csproj", "{C7B03DDF-7342-9BBA-E63A-8B911F79F8F4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Providers.Gemini", "src\SmartHopper.Providers.Gemini\SmartHopper.Providers.Gemini.csproj", "{087FFA5E-1049-459D-9C68-1C0B8E7F9EBC}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Solution file swaps OpenAI project GUID and reuses old one for Gemini

In SmartHopper.sln, the OpenAI project GUID changed from {087FFA5E-1049-459D-9C68-1C0B8E7F9EBC} to {C7B03DDF-7342-9BBA-E63A-8B911F79F8F4}, and the old OpenAI GUID {087FFA5E-...} is now used for the new Gemini project. While solution-level project GUIDs are just internal references and this is not a functional bug, it's unusual to swap GUIDs between projects rather than generating a fresh one for the new project. This could cause confusion in version control and build tooling.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 176 to 193
if (Uri.TryCreate(imageUrl, UriKind.Absolute, out var uri))
{
this.SetResult(uri, imageData, revisedPrompt);
return;
}
}

// Handle case where only imageData is provided (no URL)
if (imageData != null)
{
this.ImageData = imageData;
}

if (revisedPrompt != null)
{
this.RevisedPrompt = revisedPrompt;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: AIInteractionImage.SetResult silently drops invalid URL string with imageData=null

In AIInteractionImage.SetResult(string, string, string) at line 173-181, when imageUrl is a non-null string that fails Uri.TryCreate (e.g., a relative path or malformed URL), and imageData is also null, the method silently completes without setting any image data and without throwing. The precondition check at line 168 only throws when BOTH are null. A call like SetResult("not-a-url") would pass the null check (imageUrl is non-null), fail the Uri parse, skip the imageData branch (it's null), and return with no image data set. This edge case existed before the PR but the added return statement at line 179 and the new fallback logic at lines 183-192 were added to address image-data-only cases.

(Refers to lines 166-193)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +398 to +446
else if (interaction is AIInteractionImage imageInteraction)
{
if (!string.IsNullOrWhiteSpace(imageInteraction.ImageData))
{
parts.Add(new JObject
{
{
"inline_data", new JObject
{
{ "mime_type", imageInteraction.MimeType ?? "image/png" },
{ "data", imageInteraction.ImageData },
}
},
});
}
else if (imageInteraction.ImageUrl != null)
{
// Fetch image from URL and convert to base64 inline data
var (base64Data, mimeType) = this.FetchImageFromUrl(imageInteraction.ImageUrl);
if (!string.IsNullOrWhiteSpace(base64Data))
{
parts.Add(new JObject
{
{
"inline_data", new JObject
{
{ "mime_type", mimeType },
{ "data", base64Data },
}
},
});
}
else
{
// Fallback: add URL as text if fetch fails
parts.Add(new JObject { { "text", imageInteraction.ImageUrl.ToString() } });
Debug.WriteLine($"[GeminiProvider] Warning: Failed to fetch image from URL, sending URL as text: {imageInteraction.ImageUrl}");
}
}
}

if (parts.Count > 0)
{
contents.Add(new JObject
{
{ "role", role },
{ "parts", parts },
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: GeminiProvider.Encode duplicates image-handling logic between single-interaction and full-request paths

The Encode(IAIInteraction) method (GeminiProvider.cs:170) and Encode(AIRequestCall) method (GeminiProvider.cs:306) both contain nearly identical image encoding logic (inline_data for base64, URL fetch fallback). The single-interaction encoder calls EncodeToJToken which has the logic, but the full-request encoder re-implements it inline instead of delegating. This creates a maintenance risk where a fix to one path might not be applied to the other. Consider extracting the image part encoding to a shared private method.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +583 to +587
// NOTE: reasoning_effort is incompatible with function tools on gpt-5.4, so omit it when tools are present
if (OSeriesModelRegex().IsMatch(request.Model) || Gpt5ModelRegex().IsMatch(request.Model))
{
requestBody["reasoning_effort"] = reasoningEffort;
// Only add reasoning_effort if no tools are present (gpt-5.4 incompatibility)
if (!hasTools)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: OpenAI reasoning_effort omitted when tools are present on gpt-5 models

At OpenAIProvider.cs:583-587, reasoning_effort is explicitly omitted when tools are present (if (!hasTools) { requestBody["reasoning_effort"] = reasoningEffort; }). The comment says this is due to a "gpt-5.4 incompatibility". This is a deliberate design decision but changes observable behavior: users requesting reasoning with function calling on o-series/gpt-5 models will silently get no reasoning effort parameter. Consider surfacing a diagnostic message when this suppression occurs.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants