diff --git a/.github/ISSUE_TEMPLATE/model-verification.yml b/.github/ISSUE_TEMPLATE/model-verification.yml index 15a6fbbdc..30989fac4 100644 --- a/.github/ISSUE_TEMPLATE/model-verification.yml +++ b/.github/ISSUE_TEMPLATE/model-verification.yml @@ -61,42 +61,44 @@ body: - Anthropic / claude-sonnet-5 - Anthropic / claude-opus-5 - Anthropic / claude-opus-4-8 - - Anthropic / claude-opus-4-7 - Anthropic / claude-fable-5 + - Anthropic / claude-opus-4-7 - Anthropic / claude-sonnet-4-6 - Anthropic / claude-opus-4-6 - - Anthropic / claude-haiku-4-5-20251001 - Anthropic / claude-opus-4-5-20251101 + - Anthropic / claude-haiku-4-5-20251001 - Anthropic / claude-sonnet-4-5-20250929 + - DeepSeek / deepseek-v4-flash-vision-exp - DeepSeek / deepseek-v4-flash - DeepSeek / deepseek-v4-pro + - Gemini / gemini-3.7-flash + - Gemini / gemini-3.5-flash-lite + - Gemini / gemini-3.6-flash - Gemini / gemini-3.1-flash-lite-image - - Gemini / gemini-3.1-flash-lite - Gemini / gemini-3.1-flash-image - - Gemini / gemini-3.5-flash - Gemini / gemini-3-pro-image - Gemini / lyria-3-pro-preview - Gemini / lyria-3-clip-preview - Gemini / gemma-4-26b-a4b-it - Gemini / gemma-4-31b-it + - Gemini / gemini-3.1-flash-lite - Gemini / gemini-3.1-flash-lite-preview - - Gemini / gemini-3.1-flash-image-preview - - Gemini / gemini-3.1-flash-lite-preview + - Gemini / gemini-3.5-flash - Gemini / gemini-3.1-pro-preview-customtools + - Gemini / gemini-3.1-flash-image-preview + - Gemini / gemini-3-flash-preview - Gemini / gemini-3.1-pro-preview - Gemini / gemini-2.5-flash-image - - Gemini / gemini-3-flash-preview - Gemini / gemini-3-pro-image-preview - Gemini / gemini-2.5-flash-lite - - Gemini / gemini-2.5-flash-lite-preview-09-2025 - Gemini / gemini-2.5-flash - Gemini / gemini-2.5-pro - - Gemini / gemma-3n-e4b-it - Gemini / gemini-2.5-pro-preview - - Gemini / gemini-2.5-pro-preview-05-06 - Gemini / gemma-3-4b-it + - Gemini / gemma-3n-e4b-it - Gemini / gemma-3-12b-it - Gemini / gemma-3-27b-it + - Gemini / gemini-2.5-pro-preview-05-06 - Gemini / gemma-2-27b-it - MistralAI / mistral-small-2603 - MistralAI / mistral-medium-latest @@ -104,68 +106,56 @@ body: - MistralAI / ministral-8b-2512 - MistralAI / ministral-14b-2512 - MistralAI / mistral-large-2512 - - MistralAI / devstral-2512 - MistralAI / codestral-2508 - - MistralAI / mistral-medium-2508 - MistralAI / codestral-embed + - MistralAI / glm-5-2 - MistralAI / labs-leanstral-1-5-1 - - MistralAI / magistral-medium-2509 - MistralAI / mistral-embed-2312 - - MistralAI / mistral-medium-2505 - MistralAI / mistral-ocr-2512 - - MistralAI / voxtral-small-2507 + - MistralAI / mistral-ocr-4-0 - MistralAI / mistral-ocr-latest - - MistralAI / open-mistral-nemo - - MistralAI / voxtral-mini-2507 - MistralAI / voxtral-mini-2602 - MistralAI / voxtral-mini-tts-2603 - MistralAI / voxtral-small-2507 - - OpenAI / gpt-5.5-2026-04-23 - - OpenAI / gpt-5.5-pro-2026-04-23 + - OpenAI / gpt-5.6-luna + - OpenAI / gpt-5.6-sol + - OpenAI / gpt-5.6-terra - OpenAI / gpt-5.4-nano-2026-03-17 - - OpenAI / gpt-audio-mini-2025-12-15 - OpenAI / gpt-5.4-mini-2026-03-17 - - OpenAI / gpt-5.3-chat-latest - OpenAI / gpt-5.3-codex - - OpenAI / gpt-5.2-codex - OpenAI / gpt-5.4-2026-03-05 - OpenAI / gpt-5.5-2026-04-23 - OpenAI / gpt-5.5-pro-2026-04-23 - OpenAI / gpt-5.4-pro-2026-03-05 - - OpenAI / gpt-5.1-codex-mini - - OpenAI / o4-mini-deep-research-2025-06-26 + - OpenAI / gpt-audio-mini-2025-12-15 - OpenAI / gpt-5.1-codex-max - - OpenAI / gpt-5.1-2025-11-13 - - OpenAI / gpt-5.1-codex + - OpenAI / gpt-5.2-codex - OpenAI / gpt-5.2-chat-latest - OpenAI / gpt-5.2-2025-12-11 - - OpenAI / o3-deep-research-2025-06-26 - - OpenAI / gpt-5-pro-2025-10-06 + - OpenAI / gpt-audio-2025-08-28 - OpenAI / gpt-5.2-pro-2025-12-11 + - OpenAI / gpt-5.1-codex-mini + - OpenAI / gpt-5.1-2025-11-13 + - OpenAI / gpt-5.1-codex + - OpenAI / gpt-5-pro-2025-10-06 - OpenAI / gpt-5-mini-2025-08-07 - OpenAI / gpt-5-nano-2025-08-07 - - OpenAI / gpt-5-codex - - OpenAI / gpt-5-chat-latest - OpenAI / gpt-5-2025-08-07 + - OpenAI / o3-pro-2025-06-10 - OpenAI / gpt-4.1-nano-2025-04-14 - OpenAI / gpt-4.1-mini-2025-04-14 - OpenAI / o4-mini-2025-04-16 - OpenAI / o3-2025-04-16 - OpenAI / gpt-4.1-2025-04-14 - - OpenAI / o3-pro-2025-06-10 - - OpenAI / gpt-4o-mini-search-preview-2025-03-11 - - OpenAI / o3-mini-2025-01-31 - - OpenAI / gpt-4o-search-preview-2025-03-11 - OpenAI / o1-pro-2025-03-19 - OpenAI / o3-mini-2025-01-31 - OpenAI / o1-2024-12-17 - OpenAI / gpt-4o-mini-2024-07-18 - - OpenAI / gpt-4o-2024-08-06 - OpenAI / gpt-3.5-turbo-instruct - OpenAI / gpt-3.5-turbo-16k + - OpenAI / gpt-4o-2024-08-06 - OpenAI / gpt-4o-2024-05-13 - OpenAI / chat-latest - - OpenAI / gpt-4o-2024-08-06 - OpenAI / chatgpt-image-latest - OpenAI / gpt-4o-mini-search-preview-2025-03-11 - OpenAI / gpt-4o-mini-transcribe-2025-03-20 @@ -179,6 +169,7 @@ body: - OpenAI / gpt-5-codex - OpenAI / gpt-5-search-api-2025-10-14 - OpenAI / gpt-5.1-chat-latest + - OpenAI / gpt-5.3-chat-latest - OpenAI / gpt-audio-1.5 - OpenAI / gpt-audio-mini-2025-10-06 - OpenAI / gpt-image-1 @@ -199,333 +190,341 @@ body: - OpenAI / tts-1-hd - OpenAI / tts-1-hd-1106 - OpenAI / whisper-1 + - OpenRouter / openrouter/auto-beta - OpenRouter / openrouter/fusion - - OpenRouter / openrouter/pareto-code + - OpenRouter / stealth/ox-alpha + - OpenRouter / liquid/lfm-2.5-2.6b:free + - OpenRouter / nvidia/nemotron-3.5-lightning:free + - OpenRouter / thinkingmachines/inkling-small:free + - OpenRouter / poolside/laguna-s-2.1:free + - OpenRouter / thinkingmachines/inkling:free - OpenRouter / poolside/laguna-xs-2.1:free - OpenRouter / cohere/north-mini-code:free + - OpenRouter / z-ai/glm-5.2:free - OpenRouter / nvidia/nemotron-3.5-content-safety:free - OpenRouter / nvidia/nemotron-3-ultra-550b-a55b:free - - OpenRouter / nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free - - OpenRouter / poolside/laguna-m.1:free - - OpenRouter / inclusionai/ling-2.6-flash - - OpenRouter / ibm-granite/granite-4.1-8b + - OpenRouter / inclusionai/ling-3.0-flash + - OpenRouter / nex-agi/nex-n2-mini + - OpenRouter / upstage/solar-pro4 - OpenRouter / poolside/laguna-xs-2.1 - - OpenRouter / deepseek/deepseek-v4-flash - - OpenRouter / tencent/hy3-preview - - OpenRouter / xiaomi/mimo-v2.5 - - OpenRouter / poolside/laguna-m.1 - - OpenRouter / inclusionai/ring-2.6-1t - - OpenRouter / inclusionai/ling-2.6-1t - - OpenRouter / deepseek/deepseek-v4-pro - - OpenRouter / xiaomi/mimo-v2.5-pro + - OpenRouter / qwen/qwen3.7-flash + - OpenRouter / ~deepseek/deepseek-v4-flash-latest + - OpenRouter / tencent/hy-mt2-1.8b + - OpenRouter / deepseek/deepseek-v4-flash-0731 + - OpenRouter / poolside/laguna-s-2.1 + - OpenRouter / meta/muse-spark-1.2-contributor + - OpenRouter / nvidia/nemotron-3.5-lightning + - OpenRouter / tencent/hy-mt2-30b-a3b + - OpenRouter / tencent/hy-mt2-7b + - OpenRouter / tencent/hy3 + - OpenRouter / kwaipilot/kat-coder-air-v2.5 + - OpenRouter / deepseek/deepseek-v4-flash-vision-exp - OpenRouter / nex-agi/nex-n2-pro - - OpenRouter / z-ai/glm-5.2 - OpenRouter / stepfun/step-3.7-flash - OpenRouter / thinkingmachines/inkling-small - OpenRouter / meituan/longcat-2.0 + - OpenRouter / openai/gpt-5.6-luna-pro + - OpenRouter / openai/gpt-5.6-luna - OpenRouter / minimax/minimax-m3 - OpenRouter / qwen/qwen3.7-plus - - OpenRouter / google/gemini-3.1-flash-lite-image - - OpenRouter / perceptron/perceptron-mk1 - - OpenRouter / x-ai/grok-build-0.1 - - OpenRouter / nvidia/nemotron-3-ultra-550b-a55b - - OpenRouter / google/gemini-3.1-flash-lite + - OpenRouter / aion-labs/aion-3.0-mini + - OpenRouter / meta/muse-glimmer-30b + - OpenRouter / bytedance-seed/seed-2-1-turbo + - OpenRouter / google/gemini-3.7-flash + - OpenRouter / kwaipilot/kat-coder-pro-v2.5 + - OpenRouter / qwen/qwen3.8-27b + - OpenRouter / bytedance-seed/seed-2.0-code - OpenRouter / z-ai/glm-5.2 - - OpenRouter / qwen/qwen3.6-27b - - OpenRouter / x-ai/grok-4.3 - - OpenRouter / google/gemini-3.1-flash-image - - OpenRouter / z-ai/glm-5.1 - - OpenRouter / ~moonshotai/kimi-latest - - OpenRouter / moonshotai/kimi-k2.6 + - OpenRouter / google/gemini-3.5-flash-lite + - OpenRouter / deepseek/deepseek-v4-pro-0813 - OpenRouter / moonshotai/kimi-k2.7-code - - OpenRouter / qwen/qwen3.7-max - - OpenRouter / ~openai/gpt-mini-latest - - OpenRouter / ~anthropic/claude-haiku-latest - - OpenRouter / qwen/qwen3.6-max-preview - - OpenRouter / mistralai/mistral-medium-3-5 - - OpenRouter / anthropic/claude-sonnet-5 + - OpenRouter / nvidia/nemotron-3-ultra-550b-a55b + - OpenRouter / sakana/sakana-namazu + - OpenRouter / thinkingmachines/inkling + - OpenRouter / meta/muse-spark-1.2 + - OpenRouter / meta/muse-spark-1.1 + - OpenRouter / ~z-ai/glm-latest + - OpenRouter / z-ai/glm-5.3 - OpenRouter / google/gemini-3.6-flash - - OpenRouter / google/gemini-3.5-flash - - OpenRouter / ~google/gemini-flash-latest - - OpenRouter / openai/gpt-5.4-image-2 - - OpenRouter / google/gemini-3-pro-image - - OpenRouter / ~google/gemini-pro-latest + - OpenRouter / qwen/qwen3.8-2.4t-a95b + - OpenRouter / x-ai/grok-4.6 + - OpenRouter / qwen/qwen3.8-max + - OpenRouter / x-ai/grok-4.5 + - OpenRouter / ~x-ai/grok-latest + - OpenRouter / aion-labs/aion-3.0 + - OpenRouter / openai/gpt-5.6-sol-pro + - OpenRouter / openai/gpt-5.6-sol + - OpenRouter / anthropic/claude-sonnet-5 + - OpenRouter / openai/gpt-5.6-terra-pro + - OpenRouter / openai/gpt-5.6-terra + - OpenRouter / moonshotai/kimi-k3 + - OpenRouter / anthropic/claude-opus-5 - OpenRouter / anthropic/claude-opus-4.8 - - OpenRouter / ~anthropic/claude-opus-latest - - OpenRouter / anthropic/claude-opus-4.7 - OpenRouter / sakana/fugu-ultra - - OpenRouter / openai/gpt-chat-latest - - OpenRouter / ~openai/gpt-latest - - OpenRouter / openai/gpt-5.5 + - OpenRouter / google/gemini-3.1-flash-lite-image + - OpenRouter / anthropic/claude-opus-5-fast - OpenRouter / ~anthropic/claude-fable-latest - OpenRouter / anthropic/claude-fable-5 - OpenRouter / anthropic/claude-opus-4.8-fast - - OpenRouter / anthropic/claude-opus-4.7-fast - - OpenRouter / openai/gpt-5.5-pro + - OpenRouter / google/gemini-3.1-flash-image + - OpenRouter / google/gemini-3-pro-image + - OpenRouter / openrouter/pareto-code + - OpenRouter / nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free - OpenRouter / google/gemma-4-26b-a4b-it:free - OpenRouter / google/gemma-4-31b-it:free - OpenRouter / google/lyria-3-pro-preview - OpenRouter / google/lyria-3-clip-preview - OpenRouter / nvidia/nemotron-3-super-120b-a12b:free - - OpenRouter / openrouter/free - - OpenRouter / liquid/lfm-2.5-1.2b-thinking:free - - OpenRouter / liquid/lfm-2.5-1.2b-instruct:free + - OpenRouter / ibm-granite/granite-4.1-8b - OpenRouter / rekaai/reka-edge - - OpenRouter / liquid/lfm-2-24b-a2b + - OpenRouter / deepseek/deepseek-v4-flash - OpenRouter / qwen/qwen3.5-9b - OpenRouter / qwen/qwen3.5-flash-02-23 - - OpenRouter / stepfun/step-3.5-flash + - OpenRouter / xiaomi/mimo-v2.5 - OpenRouter / google/gemma-4-26b-a4b-it - OpenRouter / google/gemma-4-31b-it - - OpenRouter / bytedance-seed/seed-2.0-mini - - OpenRouter / z-ai/glm-4.7-flash - OpenRouter / nvidia/nemotron-3-super-120b-a12b - - OpenRouter / minimax/minimax-m2.5 + - OpenRouter / bytedance-seed/seed-2.0-mini + - OpenRouter / tencent/hy3-preview - OpenRouter / mistralai/mistral-small-2603 - - OpenRouter / upstage/solar-pro-3 - - OpenRouter / minimax/minimax-m2.7 - OpenRouter / inception/mercury-2 + - OpenRouter / deepseek/deepseek-v4-pro - OpenRouter / arcee-ai/trinity-large-thinking - - OpenRouter / qwen/qwen3-coder-next - - OpenRouter / qwen/qwen3.5-35b-a3b + - OpenRouter / xiaomi/mimo-v2.5-pro + - OpenRouter / qwen/qwen3.6-35b-a3b - OpenRouter / qwen/qwen3.6-flash - OpenRouter / kwaipilot/kat-coder-pro-v2 + - OpenRouter / minimax/minimax-m2.7 - OpenRouter / openai/gpt-5.4-nano + - OpenRouter / qwen/qwen3.5-35b-a3b + - OpenRouter / perceptron/perceptron-mk1 - OpenRouter / qwen/qwen3.5-27b - - OpenRouter / qwen/qwen3.5-plus-02-15 - OpenRouter / aion-labs/aion-2.0 - - OpenRouter / z-ai/glm-5 + - OpenRouter / qwen/qwen3.5-plus-20260420 - OpenRouter / qwen/qwen3.6-plus - - OpenRouter / qwen/qwen3.6-27b + - OpenRouter / x-ai/grok-build-0.1 - OpenRouter / bytedance-seed/seed-2.0-lite - - OpenRouter / moonshotai/kimi-k2.5 - OpenRouter / qwen/qwen3.5-122b-a10b + - OpenRouter / google/gemini-3.1-flash-lite - OpenRouter / google/gemini-3.1-flash-lite-preview - - OpenRouter / qwen/qwen3.5-397b-a17b - OpenRouter / x-ai/grok-4.3 - OpenRouter / x-ai/grok-4.20-multi-agent - OpenRouter / x-ai/grok-4.20 - - OpenRouter / google/gemini-3.1-flash-image-preview - - OpenRouter / openai/gpt-audio-mini - - OpenRouter / qwen/qwen3-max-thinking + - OpenRouter / ~google/gemini-flash-latest + - OpenRouter / z-ai/glm-5.1 + - OpenRouter / qwen/qwen3.6-27b + - OpenRouter / moonshotai/kimi-k2.6 - OpenRouter / z-ai/glm-5-turbo + - OpenRouter / qwen/qwen3.7-max - OpenRouter / ~openai/gpt-mini-latest - OpenRouter / openai/gpt-5.4-mini - - OpenRouter / writer/palmyra-x5 - - OpenRouter / openai/gpt-5.3-chat + - OpenRouter / ~anthropic/claude-haiku-latest + - OpenRouter / qwen/qwen3.6-max-preview + - OpenRouter / mistralai/mistral-medium-3-5 + - OpenRouter / ~anthropic/claude-sonnet-latest + - OpenRouter / ~openai/gpt-latest + - OpenRouter / ~moonshotai/kimi-latest + - OpenRouter / google/gemini-3.5-flash - OpenRouter / openai/gpt-5.3-codex - - OpenRouter / openai/gpt-5.2-codex - OpenRouter / openai/gpt-5.4 - - OpenRouter / anthropic/claude-sonnet-4.6 - OpenRouter / ~google/gemini-pro-latest - OpenRouter / google/gemini-3.1-pro-preview-customtools - - OpenRouter / google/gemini-3.1-pro-preview - - OpenRouter / anthropic/claude-opus-4.6 - - OpenRouter / openai/gpt-audio + - OpenRouter / ~anthropic/claude-opus-latest + - OpenRouter / anthropic/claude-opus-4.7 + - OpenRouter / openai/gpt-chat-latest + - OpenRouter / openai/gpt-5.5 + - OpenRouter / openai/gpt-5.4-image-2 + - OpenRouter / google/gemini-3.1-flash-image-preview + - OpenRouter / anthropic/claude-opus-4.7-fast + - OpenRouter / openai/gpt-5.5-pro - OpenRouter / openai/gpt-5.4-pro - OpenRouter / openrouter/bodybuilder - OpenRouter / openrouter/free - - OpenRouter / nvidia/nemotron-3-nano-30b-a3b:free - OpenRouter / mistralai/ministral-3b-2512 - OpenRouter / mistralai/ministral-8b-2512 - - OpenRouter / arcee-ai/trinity-mini - OpenRouter / nvidia/nemotron-3-nano-30b-a3b - OpenRouter / mistralai/ministral-14b-2512 + - OpenRouter / stepfun/step-3.5-flash - OpenRouter / bytedance-seed/seed-1.6-flash - - OpenRouter / openai/gpt-oss-safeguard-20b - OpenRouter / deepseek/deepseek-v3.2 - - OpenRouter / nvidia/llama-3.3-nemotron-super-49b-v1.5 - - OpenRouter / qwen/qwen3-vl-32b-instruct - - OpenRouter / qwen/qwen3-vl-8b-instruct - - OpenRouter / allenai/olmo-3-32b-think - - OpenRouter / qwen/qwen3-vl-30b-a3b-instruct + - OpenRouter / z-ai/glm-4.7-flash + - OpenRouter / upstage/solar-pro-3 + - OpenRouter / qwen/qwen3-coder-next - OpenRouter / z-ai/glm-4.6v - - OpenRouter / minimax/minimax-m2 + - OpenRouter / minimax/minimax-m2.5 + - OpenRouter / minimax/minimax-m2-her - OpenRouter / minimax/minimax-m2.1 - - OpenRouter / deepcogito/cogito-v2.1-671b - - OpenRouter / qwen/qwen3-vl-8b-thinking - OpenRouter / mistralai/mistral-large-2512 - - OpenRouter / qwen/qwen3-vl-30b-a3b-thinking + - OpenRouter / qwen/qwen3.5-plus-02-15 - OpenRouter / z-ai/glm-4.7 - - OpenRouter / google/gemini-2.5-flash-image + - OpenRouter / z-ai/glm-5 - OpenRouter / bytedance-seed/seed-1.6 - - OpenRouter / openai/gpt-5.1-codex-mini - - OpenRouter / openai/gpt-5-image-mini - - OpenRouter / amazon/nova-2-lite-v1 - - OpenRouter / moonshotai/kimi-k2-thinking - OpenRouter / moonshotai/kimi-k2.5 + - OpenRouter / qwen/qwen3.5-397b-a17b + - OpenRouter / amazon/nova-2-lite-v1 - OpenRouter / relace/relace-search - - OpenRouter / google/gemini-2.5-flash-image + - OpenRouter / qwen/qwen3-max-thinking - OpenRouter / google/gemini-3-flash-preview - OpenRouter / openai/gpt-audio-mini - OpenRouter / writer/palmyra-x5 - OpenRouter / openai/gpt-5.1-codex-max - - OpenRouter / openai/gpt-5.1 - - OpenRouter / openai/gpt-5.1-codex - - OpenRouter / openai/gpt-5-image - - OpenRouter / amazon/nova-premier-v1 + - OpenRouter / openai/gpt-5.2-codex - OpenRouter / openai/gpt-5.2 - - OpenRouter / perplexity/sonar-pro-search - - OpenRouter / google/gemini-3-pro-image-preview + - OpenRouter / anthropic/claude-sonnet-4.6 + - OpenRouter / google/gemini-3.1-pro-preview + - OpenRouter / anthropic/claude-opus-4.6 - OpenRouter / anthropic/claude-opus-4.5 - - OpenRouter / openai/o3-deep-research - - OpenRouter / mistralai/voxtral-small-24b-2507 - - OpenRouter / openai/gpt-5-pro + - OpenRouter / openai/gpt-audio - OpenRouter / openai/gpt-5.2-pro - - OpenRouter / qwen/qwen3-next-80b-a3b-instruct:free - - OpenRouter / nvidia/nemotron-nano-9b-v2:free - - OpenRouter / openai/gpt-oss-120b:free - - OpenRouter / openai/gpt-oss-20b:free - - OpenRouter / qwen/qwen3-coder:free - - OpenRouter / cognitivecomputations/dolphin-mistral-24b-venice-edition:free - - OpenRouter / qwen/qwen3-235b-a22b-2507 - - OpenRouter / openai/gpt-oss-20b - - OpenRouter / openai/gpt-oss-120b - - OpenRouter / qwen/qwen3-30b-a3b-instruct-2507 - - OpenRouter / bytedance/ui-tars-1.5-7b - - OpenRouter / qwen/qwen3-coder-30b-a3b-instruct + - OpenRouter / ibm-granite/granite-4.0-h-micro + - OpenRouter / openai/gpt-oss-safeguard-20b - OpenRouter / nousresearch/hermes-4-70b - - OpenRouter / openai/gpt-5-nano - OpenRouter / deepseek/deepseek-v3.2-exp + - OpenRouter / qwen/qwen3-vl-32b-instruct + - OpenRouter / qwen/qwen3-vl-8b-instruct + - OpenRouter / allenai/olmo-3-32b-think - OpenRouter / thedrummer/cydonia-24b-v4.1 - - OpenRouter / tencent/hunyuan-a13b-instruct - - OpenRouter / qwen/qwen3-next-80b-a3b-thinking + - OpenRouter / qwen/qwen3-vl-30b-a3b-instruct - OpenRouter / qwen/qwen-plus-2025-07-28 - OpenRouter / qwen/qwen-plus-2025-07-28:thinking - - OpenRouter / deepseek/deepseek-chat-v3.1 - - OpenRouter / google/gemini-2.5-flash-lite-preview-09-2025 - - OpenRouter / google/gemini-2.5-flash-lite - - OpenRouter / z-ai/glm-4.5-air - - OpenRouter / qwen/qwen3-vl-235b-a22b-instruct - - OpenRouter / mistralai/codestral-2508 - - OpenRouter / deepseek/deepseek-v3.1-terminus - OpenRouter / qwen/qwen3-coder-flash - OpenRouter / deepseek/deepseek-v3.1-terminus - OpenRouter / minimax/minimax-m2 - OpenRouter / qwen/qwen3-next-80b-a3b-instruct - - OpenRouter / morph/morph-v3-fast + - OpenRouter / qwen/qwen3-next-80b-a3b-thinking - OpenRouter / relace/relace-apply-3 - - OpenRouter / qwen/qwen3-235b-a22b-thinking-2507 + - OpenRouter / qwen/qwen3-vl-235b-a22b-instruct + - OpenRouter / openai/gpt-5.1-codex-mini + - OpenRouter / qwen/qwen3-vl-8b-thinking + - OpenRouter / qwen/qwen3-vl-30b-a3b-thinking - OpenRouter / qwen/qwen3-30b-a3b-thinking-2507 - - OpenRouter / z-ai/glm-4.5v - - OpenRouter / qwen/qwen3-coder - - OpenRouter / morph/morph-v3-large - - OpenRouter / mistralai/mistral-medium-3.1 - - OpenRouter / openai/gpt-5-mini - - OpenRouter / moonshotai/kimi-k2 - - OpenRouter / qwen/qwen3-vl-235b-a22b-thinking + - OpenRouter / moonshotai/kimi-k2-thinking - OpenRouter / nousresearch/hermes-4-405b - OpenRouter / qwen/qwen3-coder-plus - - OpenRouter / switchpoint/router - OpenRouter / qwen/qwen3-max - - OpenRouter / ai21/jamba-large-1.7 - - OpenRouter / openai/gpt-5-codex - - OpenRouter / openai/gpt-5-chat - - OpenRouter / openai/gpt-5 + - OpenRouter / qwen/qwen3-vl-235b-a22b-thinking + - OpenRouter / anthropic/claude-haiku-4.5 + - OpenRouter / openai/gpt-5-image-mini + - OpenRouter / openai/gpt-5.1 + - OpenRouter / openai/gpt-5.1-codex + - OpenRouter / amazon/nova-premier-v1 + - OpenRouter / perplexity/sonar-pro-search - OpenRouter / anthropic/claude-sonnet-4.5 - - OpenRouter / anthropic/claude-opus-4.1 - - OpenRouter / google/gemma-3n-e4b-it + - OpenRouter / google/gemini-2.5-flash-image + - OpenRouter / openai/gpt-5-image + - OpenRouter / mistralai/voxtral-small-24b-2507 + - OpenRouter / openai/gpt-5-pro + - OpenRouter / google/gemini-3-pro-image-preview + - OpenRouter / openai/gpt-oss-20b + - OpenRouter / openai/gpt-oss-120b - OpenRouter / qwen/qwen3-30b-a3b-instruct-2507 - OpenRouter / bytedance/ui-tars-1.5-7b - OpenRouter / mistralai/mistral-small-3.2-24b-instruct - - OpenRouter / qwen/qwen3-14b - - OpenRouter / qwen/qwen3-32b - - OpenRouter / meta-llama/llama-4-scout - - OpenRouter / openai/gpt-4.1-nano - - OpenRouter / qwen/qwen3-8b - - OpenRouter / qwen/qwen3-30b-a3b - - OpenRouter / meta-llama/llama-4-maverick - - OpenRouter / arcee-ai/coder-large - - OpenRouter / arcee-ai/virtuoso-large + - OpenRouter / qwen/qwen3-coder-30b-a3b-instruct + - OpenRouter / openai/gpt-5-nano + - OpenRouter / qwen/qwen3-235b-a22b-2507 + - OpenRouter / tencent/hunyuan-a13b-instruct + - OpenRouter / google/gemini-2.5-flash-lite + - OpenRouter / z-ai/glm-4.5-air + - OpenRouter / mistralai/codestral-2508 + - OpenRouter / cognitivecomputations/dolphin-mistral-24b-venice-edition + - OpenRouter / qwen/qwen3-coder + - OpenRouter / morph/morph-v3-fast - OpenRouter / baidu/ernie-4.5-vl-424b-a47b - - OpenRouter / openai/gpt-4.1-mini - - OpenRouter / qwen/qwen3-235b-a22b - - OpenRouter / mistralai/mistral-medium-3 + - OpenRouter / deepseek/deepseek-chat-v3.1 + - OpenRouter / morph/morph-v3-large + - OpenRouter / mistralai/mistral-medium-3.1 + - OpenRouter / openai/gpt-5-mini - OpenRouter / deepseek/deepseek-r1-0528 - OpenRouter / minimax/minimax-m1 - OpenRouter / qwen/qwen3-235b-a22b-thinking-2507 - OpenRouter / moonshotai/kimi-k2 - OpenRouter / google/gemini-2.5-flash + - OpenRouter / openai/gpt-5 - OpenRouter / google/gemini-2.5-pro - OpenRouter / google/gemini-2.5-pro-preview - - OpenRouter / google/gemini-2.5-pro-preview-05-06 - - OpenRouter / anthropic/claude-sonnet-4 - - OpenRouter / anthropic/claude-opus-4 + - OpenRouter / anthropic/claude-opus-4.1 - OpenRouter / openai/o3-pro - OpenRouter / google/gemma-3-4b-it - - OpenRouter / microsoft/phi-4 + - OpenRouter / google/gemma-3n-e4b-it - OpenRouter / google/gemma-3-12b-it - - OpenRouter / google/gemma-3-27b-it + - OpenRouter / meta-llama/llama-guard-4-12b - OpenRouter / rekaai/reka-flash-3 + - OpenRouter / qwen/qwen3-14b - OpenRouter / qwen/qwen3-32b - OpenRouter / meta-llama/llama-4-scout - OpenRouter / openai/gpt-4.1-nano - - OpenRouter / microsoft/phi-4 - OpenRouter / google/gemma-3-27b-it - OpenRouter / qwen/qwen3-8b - OpenRouter / qwen/qwen3-30b-a3b - OpenRouter / mistralai/mistral-small-3.1-24b-instruct - - OpenRouter / mistralai/mistral-saba - - OpenRouter / qwen/qwen-plus + - OpenRouter / meta-llama/llama-4-maverick - OpenRouter / thedrummer/skyfall-36b-v2 - - OpenRouter / deepseek/deepseek-r1-distill-llama-70b - OpenRouter / deepseek/deepseek-chat-v3-0324 - - OpenRouter / qwen/qwen2.5-vl-72b-instruct - - OpenRouter / perplexity/sonar - - OpenRouter / minimax/minimax-01 - - OpenRouter / aion-labs/aion-1.0-mini - - OpenRouter / aion-labs/aion-rp-llama-3.1-8b - - OpenRouter / deepseek/deepseek-r1 - - OpenRouter / sao10k/l3.1-70b-hanami-x1 - - OpenRouter / openai/o3-mini-high - - OpenRouter / openai/o3-mini + - OpenRouter / arcee-ai/virtuoso-large + - OpenRouter / openai/gpt-4.1-mini + - OpenRouter / qwen/qwen3-235b-a22b + - OpenRouter / mistralai/mistral-medium-3 + - OpenRouter / openai/o4-mini-high + - OpenRouter / openai/o4-mini + - OpenRouter / openai/o3 + - OpenRouter / openai/gpt-4.1 - OpenRouter / perplexity/sonar-reasoning-pro - OpenRouter / perplexity/sonar-deep-research - - OpenRouter / aion-labs/aion-1.0 - OpenRouter / cohere/command-a + - OpenRouter / google/gemini-2.5-pro-preview-05-06 + - OpenRouter / anthropic/claude-sonnet-4 - OpenRouter / perplexity/sonar-pro + - OpenRouter / anthropic/claude-opus-4 - OpenRouter / openai/o1-pro - - OpenRouter / meta-llama/llama-3.3-70b-instruct:free - - OpenRouter / qwen/qwen-2.5-7b-instruct + - OpenRouter / mistralai/mistral-small-24b-instruct-2501 + - OpenRouter / microsoft/phi-4 - OpenRouter / amazon/nova-micro-v1 - OpenRouter / cohere/command-r7b-12-2024 - OpenRouter / amazon/nova-lite-v1 - OpenRouter / meta-llama/llama-3.3-70b-instruct - - OpenRouter / thedrummer/unslopnemo-12b + - OpenRouter / mistralai/mistral-saba - OpenRouter / sao10k/l3.3-euryale-70b + - OpenRouter / qwen/qwen-plus + - OpenRouter / deepseek/deepseek-r1-distill-llama-70b + - OpenRouter / qwen/qwen2.5-vl-72b-instruct + - OpenRouter / perplexity/sonar - OpenRouter / deepseek/deepseek-chat - - OpenRouter / qwen/qwen-2.5-coder-32b-instruct + - OpenRouter / minimax/minimax-01 + - OpenRouter / aion-labs/aion-rp-llama-3.1-8b + - OpenRouter / deepseek/deepseek-r1 - OpenRouter / amazon/nova-pro-v1 + - OpenRouter / openai/o3-mini-high - OpenRouter / openai/o3-mini - - OpenRouter / mistralai/mistral-large-2407 - - OpenRouter / openai/gpt-4o-2024-11-20 - - OpenRouter / inflection/inflection-3-pi - - OpenRouter / inflection/inflection-3-productivity - OpenRouter / openai/o1 - - OpenRouter / meta-llama/llama-3.2-3b-instruct:free - - OpenRouter / nousresearch/hermes-3-llama-3.1-405b:free - - OpenRouter / meta-llama/llama-3.1-8b-instruct - - OpenRouter / mistralai/mistral-nemo - - OpenRouter / sao10k/l3-lunaris-8b + - OpenRouter / mistralai/ministral-8b + - OpenRouter / qwen/qwen-2.5-7b-instruct - OpenRouter / meta-llama/llama-3.2-1b-instruct - OpenRouter / meta-llama/llama-3.2-3b-instruct - - OpenRouter / meta-llama/llama-3.2-11b-vision-instruct + - OpenRouter / thedrummer/unslopnemo-12b - OpenRouter / qwen/qwen-2.5-72b-instruct - OpenRouter / thedrummer/rocinante-12b - OpenRouter / cohere/command-r-08-2024 - - OpenRouter / openai/gpt-4o-mini - - OpenRouter / openai/gpt-4o-mini-2024-07-18 - - OpenRouter / google/gemma-2-27b-it - - OpenRouter / nousresearch/hermes-3-llama-3.1-70b - OpenRouter / sao10k/l3.1-euryale-70b - - OpenRouter / nousresearch/hermes-3-llama-3.1-405b + - OpenRouter / qwen/qwen-2.5-coder-32b-instruct + - OpenRouter / anthracite-org/magnum-v4-72b + - OpenRouter / mistralai/mistral-large-2407 + - OpenRouter / openai/gpt-4o-2024-11-20 - OpenRouter / cohere/command-r-plus-08-2024 - - OpenRouter / openai/gpt-4o-2024-08-06 - OpenRouter / openrouter/auto + - OpenRouter / mistralai/mistral-nemo + - OpenRouter / sao10k/l3-lunaris-8b - OpenRouter / gryphe/mythomax-l2-13b + - OpenRouter / meta-llama/llama-3.1-8b-instruct + - OpenRouter / meta-llama/llama-3.1-70b-instruct + - OpenRouter / openai/gpt-4o-mini + - OpenRouter / openai/gpt-4o-mini-2024-07-18 - OpenRouter / microsoft/wizardlm-2-8x22b + - OpenRouter / google/gemma-2-27b-it - OpenRouter / undi95/remm-slerp-l2-13b + - OpenRouter / nousresearch/hermes-3-llama-3.1-70b - OpenRouter / mancer/weaver + - OpenRouter / nousresearch/hermes-3-llama-3.1-405b - OpenRouter / anthropic/claude-3-haiku - OpenRouter / openai/gpt-3.5-turbo - OpenRouter / openai/gpt-3.5-turbo-0613 @@ -533,6 +532,7 @@ body: - OpenRouter / openai/gpt-3.5-turbo-16k - OpenRouter / mistralai/mixtral-8x22b-instruct - OpenRouter / mistralai/mistral-large + - OpenRouter / openai/gpt-4o-2024-08-06 - OpenRouter / openai/gpt-4o - OpenRouter / openai/gpt-4o-2024-05-13 - OpenRouter / openai/gpt-4-turbo diff --git a/.github/actions/documentation/generate-release-notes/action.yml b/.github/actions/documentation/generate-release-notes/action.yml index 0c9838976..5be187ca6 100644 --- a/.github/actions/documentation/generate-release-notes/action.yml +++ b/.github/actions/documentation/generate-release-notes/action.yml @@ -118,7 +118,7 @@ runs: ## Technical Requirements - Rhino 8.0 or above is required - Windows 10/11 or macOS - - Valid API keys for MistralAI, OpenAI, DeepSeek, Anthropic, OpenRouter or Gemini + - Valid API keys for MistralAI, OpenAI, DeepSeek, Anthropic, OpenRouter, Gemini, or a self-hosted LocalAI/Ollama endpoint ## Important Notes ${STAGE_LINE} diff --git a/.github/labeler.yml b/.github/labeler.yml index ed51a2b78..1822ff94d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -145,6 +145,9 @@ documentation: "component: AI File2Md": - changed-files: - any-glob-to-any-file: 'src/SmartHopper.Components/Knowledge/AIFile2MdComponent.cs' +"component: AI FileContext": + - changed-files: + - any-glob-to-any-file: 'src/SmartHopper.Components/AI/AIFileContextComponent.cs' "component: AI FileMetadata": - changed-files: - any-glob-to-any-file: 'src/SmartHopper.Components/AI/AIFileMetadataComponent.cs' diff --git a/.github/labels.yml b/.github/labels.yml index 82eec4383..367db7246 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -119,6 +119,9 @@ - name: "component: AI File2Md" color: "1B2638" description: "Issues related to the AI File2Md component" +- name: "component: AI FileContext" + color: "1B2638" + description: "Issues related to the AI FileContext component" - name: "component: AI FileMetadata" color: "1B2638" description: "Issues related to the AI FileMetadata component" @@ -459,6 +462,9 @@ - name: "scope: Dialogs" color: "000000" description: "Issues related to the Dialogs area" +- name: "scope: Hosting" + color: "000000" + description: "Issues related to the Hosting area" - name: "scope: Initialization" color: "000000" description: "Issues related to the Initialization area" @@ -477,18 +483,12 @@ - name: "scope: Settings" color: "000000" description: "Issues related to the Settings area" -- name: "scope: Streaming" - color: "000000" - description: "Issues related to the Streaming area" - name: "scope: Types" color: "000000" description: "Issues related to the Types area" - name: "scope: UI" color: "000000" description: "Issues related to the UI area" -- name: "scope: Utilities" - color: "000000" - description: "Issues related to the Utilities area" - name: "scope: Utils" color: "000000" description: "Issues related to the Utils area" diff --git a/.github/workflows/chore-cleanup-stale-branches.yml b/.github/workflows/chore-cleanup-stale-branches.yml index ee4054bf1..dd0d04929 100644 --- a/.github/workflows/chore-cleanup-stale-branches.yml +++ b/.github/workflows/chore-cleanup-stale-branches.yml @@ -1,10 +1,11 @@ -name: 🧹 Cleanup stale chore branches +name: 🧹 Cleanup stale chore/cascade/devin branches -# Description: Scheduled weekly cleanup that removes chore/* branches with no +# Description: Scheduled weekly cleanup that removes stale branches with no # open pull requests. A branch is deleted only when ALL of these are true: -# 1. Its name starts with "chore/". +# 1. Its name starts with one of the watched prefixes (chore/, cascade/, devin/). # 2. It has zero open PRs (merged/closed PRs are fine). # 3. It is not a base (protected) branch. +# 4. Its last commit is older than the configured age threshold. # # Triggers: # - schedule: every Monday 4:00 UTC @@ -12,30 +13,48 @@ name: 🧹 Cleanup stale chore branches # # Permissions: # - contents: write - Required to delete branches +# - pull-requests: read - Required to check for open PRs on: schedule: - cron: '0 4 * * 1' workflow_dispatch: + inputs: + max-age-days: + description: 'Delete branches whose last commit is older than this many days' + required: false + type: string + default: '7' permissions: contents: write + pull-requests: read jobs: cleanup: - name: Delete orphaned chore branches + name: Delete orphaned chore/cascade/devin branches runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Find and delete stale chore branches + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Find and delete stale branches uses: actions/github-script@v7 + env: + MAX_AGE_DAYS: ${{ github.event.inputs.max-age-days || '7' }} with: script: | const owner = context.repo.owner; const repo = context.repo.repo; - const prefix = 'chore/'; - let deleted = 0; - let skipped = 0; + const maxAgeDays = parseInt(process.env.MAX_AGE_DAYS, 10); + const prefixes = ['chore/', 'cascade/', 'devin/']; + const protectedBranches = new Set(['main', 'dev']); + + const now = new Date(); + const cutoff = new Date(now.getTime() - maxAgeDays * 24 * 60 * 60 * 1000); // Paginate through all branches const branches = await github.paginate( @@ -43,10 +62,15 @@ jobs: { owner, repo, per_page: 100 }, ); - const choreBranches = branches.filter(b => b.name.startsWith(prefix)); - core.info(`Found ${choreBranches.length} chore/* branch(es).`); + const watchedBranches = branches.filter(b => + prefixes.some(p => b.name.startsWith(p)) && !protectedBranches.has(b.name) + ); + + let deleted = 0; + let skipped = 0; + let tooNew = 0; - for (const branch of choreBranches) { + for (const branch of watchedBranches) { // Check for open PRs from this branch const { data: openPrs } = await github.rest.pulls.list({ owner, @@ -62,7 +86,21 @@ jobs: continue; } - // No open PRs — safe to delete + // Get the commit date for the branch tip + const { data: commit } = await github.rest.git.getCommit({ + owner, + repo, + commit_sha: branch.commit.sha, + }); + + const commitDate = new Date(commit.committer.date); + if (commitDate > cutoff) { + core.info(`⏭️ ${branch.name} — last commit ${commitDate.toISOString()} is newer than ${maxAgeDays} days, skipping.`); + tooNew++; + continue; + } + + // No open PRs and old enough — safe to delete try { await github.rest.git.deleteRef({ owner, @@ -77,13 +115,14 @@ jobs: } core.summary - .addHeading('Stale chore branch cleanup', 3) + .addHeading('Stale branch cleanup', 3) .addTable([ [{ data: 'Metric', header: true }, { data: 'Count', header: true }], - ['Chore branches found', `${choreBranches.length}`], + ['Watched branches found', `${watchedBranches.length}`], ['Deleted', `${deleted}`], ['Skipped (open PRs)', `${skipped}`], + ['Skipped (too new)', `${tooNew}`], ]) .write(); - core.info(`Done. Deleted ${deleted}, skipped ${skipped}.`); + core.info(`Done. Deleted ${deleted}, skipped ${skipped}, too new ${tooNew}.`); diff --git a/.github/workflows/chore-update-ghjson-spec-docs.yml b/.github/workflows/chore-update-ghjson-spec-docs.yml index 1cead5f5b..7ec0aaab2 100644 --- a/.github/workflows/chore-update-ghjson-spec-docs.yml +++ b/.github/workflows/chore-update-ghjson-spec-docs.yml @@ -63,7 +63,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.ref_name }} fetch-depth: 0 - name: Check for spec doc drift diff --git a/.github/workflows/hotfix-backport.yml b/.github/workflows/hotfix-backport.yml new file mode 100644 index 000000000..d831ecba2 --- /dev/null +++ b/.github/workflows/hotfix-backport.yml @@ -0,0 +1,187 @@ +name: Hotfix Backport to Dev + +# Description: After a hotfix release PR merges to main, this workflow +# automatically backports the actual fix (without the release version/changelog +# changes) to dev. +# +# It uses the merged PR diff, excludes the version-specific files +# (Solution.props, CHANGELOG.md, README.md, yak-package/manifest.yml), and +# opens a backport/hotfix-X.Y.Z-description PR to dev. +# +# Related workflows: +# - hotfix-0-new-branch.yml (creates the hotfix branch) +# - hotfix-1-release-hotfix.yml (prepares the release PR to main) +# - release-3-pr-to-main-closed.yml (creates the GitHub release) +# - sync-dev-from-main.yml (brings other main changes to dev) +# +# Triggers: +# - pull_request.closed on main/main-* where the head ref is a hotfix release branch +# +# Permissions: +# - contents: write - Required to create and push the backport branch +# - pull-requests: write - Required to create the PR and enable auto-merge +# - actions: write - Required to dispatch required PR checks +# - issues: read - Required to assign the PR to the current milestone + +on: + pull_request: + types: [closed] + branches: + - main + - 'main-*' + +permissions: + contents: write + pull-requests: write + actions: write + issues: read + +concurrency: + group: hotfix-backport-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + backport-to-dev: + if: | + github.event.pull_request.merged == true && + startsWith(github.event.pull_request.head.ref, 'release/') && + contains(github.event.pull_request.head.ref, '-hotfix-') + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout dev + uses: actions/checkout@v4 + with: + ref: dev + fetch-depth: 0 + + - name: Set up Git user + run: | + git config user.name "${{ vars.SMARTHOPPER_BOT_NAME }}" + git config user.email "${{ vars.SMARTHOPPER_BOT_EMAIL }}" + + - name: Resolve hotfix version and description + id: parse + shell: bash + run: | + HEAD_REF="${{ github.event.pull_request.head.ref }}" + # release/X.Y.Z-hotfix-description -> X.Y.Z and description + if [[ "$HEAD_REF" =~ ^release/([0-9]+\.[0-9]+\.[0-9]+)-hotfix-(.*)$ ]]; then + VERSION="${BASH_REMATCH[1]}" + DESCRIPTION="${BASH_REMATCH[2]}" + else + echo "::error::Could not parse hotfix version/description from $HEAD_REF" + exit 1 + fi + BACKPORT_BRANCH="backport/hotfix-${VERSION}-${DESCRIPTION}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "description=$DESCRIPTION" >> "$GITHUB_OUTPUT" + echo "backport_branch=$BACKPORT_BRANCH" >> "$GITHUB_OUTPUT" + + - name: Create backport branch from dev + id: branch + run: | + BACKPORT_BRANCH="${{ steps.parse.outputs.backport_branch }}" + git checkout -b "$BACKPORT_BRANCH" + + - name: Apply hotfix diff to dev + id: apply + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + PR_NUMBER="${{ github.event.pull_request.number }}" + BACKPORT_BRANCH="${{ steps.parse.outputs.backport_branch }}" + + # Download the merged PR diff. The --exclude options below drop the + # version/changelog/manifest/README changes so dev keeps its own version. + gh pr diff "$PR_NUMBER" \ + --repo ${{ github.repository }} \ + > /tmp/hotfix.patch + + if [ ! -s /tmp/hotfix.patch ]; then + echo "No diff found for PR #$PR_NUMBER. Nothing to backport." + echo "applied=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if git apply \ + --exclude='Solution.props' \ + --exclude='CHANGELOG.md' \ + --exclude='README.md' \ + --exclude='yak-package/manifest.yml' \ + /tmp/hotfix.patch; then + git add -A + if git diff --cached --quiet; then + echo "The hotfix changes are already present on dev. Nothing to backport." + echo "applied=false" >> "$GITHUB_OUTPUT" + else + VERSION="${{ steps.parse.outputs.version }}" + ORIGINAL="${{ github.event.pull_request.html_url }}" + git commit -m "chore: backport hotfix ${VERSION} to dev" -m "Original: ${ORIGINAL}" + git push origin "$BACKPORT_BRANCH" + echo "applied=true" >> "$GITHUB_OUTPUT" + fi + else + echo "::error::The hotfix could not be applied cleanly to dev." + echo "This usually means the code has diverged and the fix must be backported manually." + exit 1 + fi + + - name: Create Pull Request + if: steps.apply.outputs.applied == 'true' + id: create-pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BACKPORT_BRANCH="${{ steps.parse.outputs.backport_branch }}" + VERSION="${{ steps.parse.outputs.version }}" + BODY=$'This PR backports hotfix **'"${VERSION}"$'** to `dev`.\n\n' + BODY+="Original PR: ${{ github.event.pull_request.html_url }}" + + gh pr create \ + --repo ${{ github.repository }} \ + --base dev \ + --head "$BACKPORT_BRANCH" \ + --title "chore: backport hotfix ${VERSION} to dev" \ + --body "$BODY" 2>&1 || true + + PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --base dev --head "$BACKPORT_BRANCH" --json number --jq '.[0].number') + if [ -z "$PR_NUMBER" ]; then + echo "::error::Failed to create or find backport PR" + exit 1 + fi + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + + - name: Add automated label + if: steps.create-pr.outputs.pr_number != '' + run: | + gh pr edit "${{ steps.create-pr.outputs.pr_number }}" --add-label "automated" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Assign PR to Milestone + if: steps.create-pr.outputs.pr_number != '' + uses: ./.github/actions/milestone/assign-pr + with: + pr-number: ${{ steps.create-pr.outputs.pr_number }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Dispatch required PR checks + if: steps.create-pr.outputs.pr_number != '' + uses: ./.github/actions/dispatch-required-pr-checks + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ steps.parse.outputs.backport_branch }} + pr-number: ${{ steps.create-pr.outputs.pr_number }} + base-ref: dev + pr-title: "chore: backport hotfix ${{ steps.parse.outputs.version }} to dev" + + - name: Enable auto-merge + if: steps.create-pr.outputs.pr_number != '' + run: | + gh pr merge "${{ steps.create-pr.outputs.pr_number }}" --auto --merge \ + || echo "::warning::Could not enable auto-merge. Please merge manually once approved." + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-delete-auto-branches.yml b/.github/workflows/pr-delete-auto-branches.yml index 996cc6bb5..b0f158d72 100644 --- a/.github/workflows/pr-delete-auto-branches.yml +++ b/.github/workflows/pr-delete-auto-branches.yml @@ -16,6 +16,7 @@ name: pr-delete-auto-branches # - fix/* # - hash-update/* # - patch/* +# - cascade/* # - devin/* # - backport/* # @@ -53,6 +54,7 @@ jobs: startsWith(github.event.pull_request.head.ref, 'fix/') || startsWith(github.event.pull_request.head.ref, 'hash-update/') || startsWith(github.event.pull_request.head.ref, 'patch/') || + startsWith(github.event.pull_request.head.ref, 'cascade/') || startsWith(github.event.pull_request.head.ref, 'devin/') || startsWith(github.event.pull_request.head.ref, 'backport/') runs-on: ubuntu-latest diff --git a/.github/workflows/release-2-pr-to-dev-closed.yml b/.github/workflows/release-2-pr-to-dev-closed.yml index 43081799b..87140fc2a 100644 --- a/.github/workflows/release-2-pr-to-dev-closed.yml +++ b/.github/workflows/release-2-pr-to-dev-closed.yml @@ -11,10 +11,12 @@ name: 🏁 2 PR Release to Main from Dev # Description: This workflow automatically creates a PR to merge a release branch into main (or main-X.Y.Z for stabilization paths). # When the release/* branch is merged to dev or dev-X.Y.Z. # -# Before creating the release PR, it verifies that dev is rebased onto main. -# If dev is behind main, it rebases and force-pushes dev. If the rebase fails -# (e.g. due to conflicts), the workflow run fails so the problem surfaces in -# GitHub Actions. +# Before creating the release PR, it verifies that the source dev branch +# (dev or dev-X.Y.Z) is rebased onto its matching main branch (main or main-X.Y.Z). +# If the dev branch is behind, it rebases and force-pushes the matching dev branch. +# The global `dev` branch is never touched during a stabilization release. +# If the rebase fails (e.g. due to conflicts), the workflow run fails so the +# problem surfaces in GitHub Actions. # # Triggers: # - Automatically when a PR to dev or dev-* is closed @@ -44,7 +46,7 @@ permissions: pull-requests: write concurrency: - group: dev-rebase-onto-main + group: release-2-${{ github.event.pull_request.base.ref || github.ref }} cancel-in-progress: false jobs: @@ -58,36 +60,56 @@ jobs: issues: read pull-requests: write steps: - - name: Checkout dev + - name: Resolve target branches + if: ${{ github.event_name == 'pull_request' }} + id: resolve-targets + shell: bash + run: | + set -euo pipefail + DEV_BRANCH="${{ github.event.pull_request.base.ref }}" + if [[ "$DEV_BRANCH" == dev-* ]]; then + SUFFIX="${DEV_BRANCH#dev-}" + TARGET_MAIN="main-${SUFFIX}" + else + TARGET_MAIN="main" + fi + echo "dev_branch=$DEV_BRANCH" >> "$GITHUB_OUTPUT" + echo "target_main=$TARGET_MAIN" >> "$GITHUB_OUTPUT" + echo "Source dev branch: $DEV_BRANCH" + echo "Target main branch: $TARGET_MAIN" + + - name: Checkout dev branch if: ${{ github.event_name == 'pull_request' }} uses: actions/checkout@v4 with: - ref: dev + ref: ${{ steps.resolve-targets.outputs.dev_branch }} fetch-depth: 0 - - name: Ensure dev is rebased onto main + - name: Ensure dev branch is rebased onto its matching main if: ${{ github.event_name == 'pull_request' }} id: sync-check shell: bash run: | set -euo pipefail - git fetch origin main + DEV_BRANCH="${{ steps.resolve-targets.outputs.dev_branch }}" + TARGET_MAIN="${{ steps.resolve-targets.outputs.target_main }}" + git fetch origin "$TARGET_MAIN" - if git merge-base --is-ancestor origin/main HEAD; then - echo "dev is up to date with main" + if git merge-base --is-ancestor "origin/$TARGET_MAIN" HEAD; then + echo "$DEV_BRANCH is up to date with $TARGET_MAIN" echo "sync_needed=false" >> "$GITHUB_OUTPUT" else - echo "::warning::dev is not a descendant of origin/main. A release PR cannot be created until dev is rebased onto main." + echo "::warning::$DEV_BRANCH is not a descendant of origin/$TARGET_MAIN. A release PR cannot be created until it is rebased." echo "sync_needed=true" >> "$GITHUB_OUTPUT" fi - - name: Rebase and force-push dev onto main + - name: Rebase and force-push matching dev branch onto its main if: ${{ github.event_name == 'pull_request' && steps.sync-check.outputs.sync_needed == 'true' }} id: rebase uses: ./.github/actions/utils/rebase-onto-base with: - head-ref: dev - base-ref: main + head-ref: ${{ steps.resolve-targets.outputs.dev_branch }} + base-ref: ${{ steps.resolve-targets.outputs.target_main }} push: true token: ${{ secrets.GITHUB_TOKEN }} bot-name: ${{ vars.SMARTHOPPER_BOT_NAME }} @@ -101,20 +123,13 @@ jobs: SOURCE_PR_TITLE: ${{ github.event.pull_request.head.ref }} SOURCE_PR_BODY: ${{ github.event.pull_request.body }} run: | - DEV_BRANCH="${{ github.event.pull_request.base.ref }}" - # If merging into dev-X.Y.Z, target main-X.Y.Z; otherwise target main - if [[ "$DEV_BRANCH" == dev-* ]]; then - SUFFIX="${DEV_BRANCH#dev-}" - TARGET_BRANCH="main-${SUFFIX}" - else - TARGET_BRANCH="main" - fi - MAIN_HEAD="$DEV_BRANCH" + DEV_BRANCH="${{ steps.resolve-targets.outputs.dev_branch }}" + TARGET_BRANCH="${{ steps.resolve-targets.outputs.target_main }}" echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT gh pr create \ --repo ${{ github.repository }} \ --base "$TARGET_BRANCH" \ - --head "$MAIN_HEAD" \ + --head "$DEV_BRANCH" \ --title "$SOURCE_PR_TITLE" \ --body "$SOURCE_PR_BODY" 2>&1 || true diff --git a/.github/workflows/release-3-pr-to-main-closed.yml b/.github/workflows/release-3-pr-to-main-closed.yml index 876e0041e..f58632f8d 100644 --- a/.github/workflows/release-3-pr-to-main-closed.yml +++ b/.github/workflows/release-3-pr-to-main-closed.yml @@ -151,6 +151,36 @@ jobs: suffix: ${{ steps.release_info.outputs.suffix }} mistral-api-key: ${{ secrets.MISTRAL_API_KEY }} + - name: Configure git + if: steps.dev_check.outputs.skip != 'true' + run: | + git config user.name "${{ vars.SMARTHOPPER_BOT_NAME }}" + git config user.email "${{ vars.SMARTHOPPER_BOT_EMAIL }}" + + - name: Create annotated release tag + if: steps.dev_check.outputs.skip != 'true' + shell: bash + run: | + set -euo pipefail + VERSION="${{ steps.release_info.outputs.version }}" + + # Fetch existing tags so we can tell whether the tag already exists. + git fetch --tags + + if git ls-remote --tags origin "$VERSION" | grep -q "refs/tags/$VERSION"; then + echo "Tag $VERSION already exists on origin. Skipping tag creation." + exit 0 + fi + + MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" + if [ -n "$MERGE_SHA" ]; then + git tag -a "$VERSION" -m "Release $VERSION" "$MERGE_SHA" + else + git tag -a "$VERSION" -m "Release $VERSION" + fi + git push origin "$VERSION" + echo "Created annotated tag $VERSION." + - name: Create Release if: steps.dev_check.outputs.skip != 'true' uses: softprops/action-gh-release@v2 diff --git a/.github/workflows/sync-dev-from-main.yml b/.github/workflows/sync-dev-from-main.yml index 4699b60c5..821f9711b 100644 --- a/.github/workflows/sync-dev-from-main.yml +++ b/.github/workflows/sync-dev-from-main.yml @@ -1,19 +1,27 @@ name: Sync dev from main -# Description: When `main` moves ahead of `dev`, this workflow rebases `dev` onto -# `origin/main` and force-pushes the result. `dev` is configured for squash merges -# only, so a merge commit cannot be used to bring `main` into `dev`. +# Description: When `main` moves ahead of `dev`, this workflow creates a PR that +# brings the non-version-specific changes from `main` back into `dev`. # -# The force-push must be allowed for the workflow token. If it is not, the rebase -# step fails and the whole workflow run fails, surfacing the problem in GitHub -# Actions instead of opening an issue. +# Version/changelog/manifest/README files are intentionally kept on the `dev` +# side so `dev` does not lose its development version. The resulting PR is +# opened to `dev` and auto-merge is enabled; a codeowner still has to approve +# before it merges. +# +# Related workflows: +# - release-2-pr-to-dev-closed.yml (creates the release PR from dev to main) +# - hotfix-backport.yml (backports a hotfix to dev after it merges to main) +# - pr-delete-auto-branches.yml (deletes the sync branch after the PR closes) # # Triggers: # - push to main # - workflow_dispatch # # Permissions: -# - contents: write (to push the rebased dev branch) +# - contents: write - Required to create and push the sync branch +# - pull-requests: write - Required to create the PR and enable auto-merge +# - actions: write - Required to dispatch required PR checks +# - issues: read - Required to assign the PR to the current milestone on: push: @@ -23,43 +31,229 @@ on: permissions: contents: write + pull-requests: write + actions: write + issues: read concurrency: - group: "dev-rebase-onto-main" + group: sync-dev-from-main cancel-in-progress: true jobs: - rebase-dev: - name: Rebase dev onto main + sync: runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout dev uses: actions/checkout@v4 with: - fetch-depth: 0 ref: dev + fetch-depth: 0 + + - name: Set up Git user + run: | + git config user.name "${{ vars.SMARTHOPPER_BOT_NAME }}" + git config user.email "${{ vars.SMARTHOPPER_BOT_EMAIL }}" - - name: Fetch main and check sync status + - name: Fetch main + run: git fetch origin main + + - name: Check if sync is needed id: check shell: bash run: | - git fetch origin main if git merge-base --is-ancestor origin/main HEAD; then - echo "dev is up to date with main" - echo "behind=false" >> "$GITHUB_OUTPUT" + echo "dev is already up to date with main" + echo "needed=false" >> "$GITHUB_OUTPUT" else - echo "dev is behind main and will be rebased" - echo "behind=true" >> "$GITHUB_OUTPUT" + echo "main is ahead of dev; sync needed" + echo "needed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Determine sync branch name + if: steps.check.outputs.needed == 'true' + id: branch + run: | + echo "sync_branch=chore/sync-dev-from-main-$(date +%Y%m%d-%H%M%S)" >> "$GITHUB_OUTPUT" + + - name: Backport main changes to dev + if: steps.check.outputs.needed == 'true' + id: backport + shell: bash + run: | + set -euo pipefail + SYNC_BRANCH="${{ steps.branch.outputs.sync_branch }}" + git checkout -b "$SYNC_BRANCH" + + # Files that carry a released version and should be kept on the dev side. + SKIP_FILES=( + "Solution.props" + "CHANGELOG.md" + "README.md" + "yak-package/manifest.yml" + ) + + # Commits on main that are not already in dev, in chronological order. + mapfile -t DELTA < <(git log --no-merges --cherry-pick --right-only --reverse \ + --format=%H "origin/dev...origin/main") + + APPLIED=() + SKIPPED=() + CONFLICTED=() + + for sha in "${DELTA[@]}"; do + readarray -t files < <(git diff-tree --no-commit-id --name-only -r "$sha") + + # Skip commits that only touch version/changelog/manifest/readme files. + all_skip=true + for f in "${files[@]}"; do + if [[ ! " ${SKIP_FILES[*]} " =~ " $f " ]]; then + all_skip=false + break + fi + done + + if [ "$all_skip" = true ]; then + SKIPPED+=("$sha") + continue + fi + + # Build a patch that omits the version-specific files. + patch_file="$(mktemp)" + git diff --binary "$sha"^.."$sha" -- . \ + ':!Solution.props' \ + ':!CHANGELOG.md' \ + ':!README.md' \ + ':!yak-package/manifest.yml' > "$patch_file" + + # Skip if there are no non-version changes (e.g. a release prep on a squashed branch). + if [ ! -s "$patch_file" ]; then + SKIPPED+=("$sha") + rm "$patch_file" + continue + fi + + if git apply --check "$patch_file" >/dev/null 2>&1; then + git apply "$patch_file" + git add -A + + # Only commit if there are staged changes. + if git diff --cached --quiet; then + SKIPPED+=("$sha") + else + msg_file="$(mktemp)" + git log -1 --format='%B' "$sha" > "$msg_file" + author="$(git log -1 --format='%an <%ae>' "$sha")" + git commit -F "$msg_file" --author "$author" + APPLIED+=("$sha") + rm "$msg_file" + fi + else + CONFLICTED+=("$sha") + fi + + rm "$patch_file" + done + + { + echo "applied_count=${#APPLIED[@]}" + echo "skipped_count=${#SKIPPED[@]}" + echo "conflicted_count=${#CONFLICTED[@]}" + echo "applied=${APPLIED[*]}" + echo "skipped=${SKIPPED[*]}" + echo "conflicted=${CONFLICTED[*]}" + } >> "$GITHUB_OUTPUT" + + if [ "${#APPLIED[@]}" -eq 0 ]; then + if [ "${#CONFLICTED[@]}" -gt 0 ]; then + echo "::error::All candidate commits conflict with dev." + exit 1 + fi + echo "No source changes to sync." + exit 0 fi - - name: Rebase and force-push dev onto main - if: steps.check.outputs.behind == 'true' - uses: ./.github/actions/utils/rebase-onto-base + git push origin "$SYNC_BRANCH" + + - name: Create Pull Request + if: steps.check.outputs.needed == 'true' && steps.backport.outputs.applied_count != '0' + id: create-pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + SYNC_BRANCH="${{ steps.branch.outputs.sync_branch }}" + BODY_FILE="$(mktemp)" + { + echo "This PR syncs source changes from \`main\` back into \`dev\`." + echo + echo "Version, changelog, manifest and README changes are intentionally kept on the \`dev\` side." + echo + if [ "${{ steps.backport.outputs.applied_count }}" -gt 0 ]; then + echo "### Commits applied (${{ steps.backport.outputs.applied_count }})" + for sha in ${{ steps.backport.outputs.applied }}; do + git log -1 --format='- %h %s' "$sha" + done + fi + if [ "${{ steps.backport.outputs.skipped_count }}" -gt 0 ]; then + echo + echo "### Skipped (${{ steps.backport.outputs.skipped_count }})" + echo "Version/changelog/manifest/README-only commits:" + for sha in ${{ steps.backport.outputs.skipped }}; do + git log -1 --format='- %h %s' "$sha" + done + fi + if [ "${{ steps.backport.outputs.conflicted_count }}" -gt 0 ]; then + echo + echo "### ⚠️ Conflicts (${{ steps.backport.outputs.conflicted_count }})" + echo "These commits could not be applied automatically and must be handled manually if still relevant:" + for sha in ${{ steps.backport.outputs.conflicted }}; do + git log -1 --format='- %h %s' "$sha" + done + fi + } > "$BODY_FILE" + + gh pr create \ + --repo ${{ github.repository }} \ + --base dev \ + --head "$SYNC_BRANCH" \ + --title "chore: sync dev from main" \ + --body-file "$BODY_FILE" 2>&1 || true + + PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --base dev --head "$SYNC_BRANCH" --json number --jq '.[0].number') + if [ -z "$PR_NUMBER" ]; then + echo "::error::Failed to create or find sync PR" + exit 1 + fi + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + + - name: Add automated label + if: steps.create-pr.outputs.pr_number != '' + run: | + gh pr edit "${{ steps.create-pr.outputs.pr_number }}" --add-label "automated" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Assign PR to Milestone + if: steps.create-pr.outputs.pr_number != '' + uses: ./.github/actions/milestone/assign-pr with: - head-ref: dev - base-ref: main - push: true + pr-number: ${{ steps.create-pr.outputs.pr_number }} token: ${{ secrets.GITHUB_TOKEN }} - bot-name: ${{ vars.SMARTHOPPER_BOT_NAME }} - bot-email: ${{ vars.SMARTHOPPER_BOT_EMAIL }} + + - name: Dispatch required PR checks + if: steps.create-pr.outputs.pr_number != '' + uses: ./.github/actions/dispatch-required-pr-checks + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ steps.branch.outputs.sync_branch }} + pr-number: ${{ steps.create-pr.outputs.pr_number }} + base-ref: dev + pr-title: "chore: sync dev from main" + + - name: Enable auto-merge + if: steps.create-pr.outputs.pr_number != '' + run: | + gh pr merge "${{ steps.create-pr.outputs.pr_number }}" --auto --merge \ + || echo "::warning::Could not enable auto-merge. Please merge manually once approved." + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..9a17c4e77 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,17 @@ +# Agent Notes + +## Testing + +- The `SmartHopper.ProviderSdk.Tests` project can be built and run without a strong-name key: + + ```powershell + dotnet test src/SmartHopper.ProviderSdk.Tests/SmartHopper.ProviderSdk.Tests.csproj -p:SignAssembly=false + ``` + +- All Provider SDK test classes use `[Collection("ProviderSdk")]` to disable xUnit parallelization. This lets tests safely reset shared mutable state such as `AIModelCapabilityRegistry.Instance.Models`. + +## Signing + +- The official build scripts expect `signing.snk` / `signing.pfx` and update `SmartHopperPublicKey` in `src/SmartHopper.Infrastructure/SmartHopper.Infrastructure.csproj`. +- Do not commit generated signing keys, certificates, or API keys. +- `SmartHopperPublicKey` changes made by local build tooling should be reverted before committing. diff --git a/CHANGELOG.md b/CHANGELOG.md index 69080915d..abec210e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,18 +9,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **LocalAI provider** (`SmartHopper.Providers.LocalAI`): new built-in provider for self-hosted [LocalAI](https://localai.io/) instances. Talks to the standard OpenAI-compatible `/v1/chat/completions` endpoint, with full support for tools/function calling, streaming, JSON Schema structured outputs, and an `Authorization: Bearer` API key when one is configured. Settings include a required **Base URL** (default `http://localhost:8080/v1`) plus optional API Key, Model, Streaming, Max Tokens, and Temperature. The provider does not auto-discover models — users list whatever models they have installed locally. +- **Ollama provider** (`SmartHopper.Providers.Ollama`): new built-in provider for [Ollama](https://ollama.com/) running locally via its [OpenAI-compatible endpoint](https://ollama.com/blog/openai-compatibility). Mirrors the LocalAI capabilities (tools, streaming, JSON Schema, optional bearer token for reverse-proxy setups) plus an extra `seed` extra-descriptor for reproducible sampling. Settings include a required **Base URL** (default `http://localhost:11434/v1`) plus optional API Key, Model (e.g. `llama3.1`, `qwen2.5:14b`, `mistral:7b-instruct` — install with `ollama pull `), Streaming, Max Tokens, and Temperature. +- Added reusable model-level batch metadata: `AIModelCapabilities.SupportsBatch` and `AIModelCapabilities.BatchPricing` for provider-agnostic batch compatibility and discounted batch pricing. +- Implemented full `IAIBatchProvider` contract on `OpenRouterProvider`, including batch submission, status polling, cancellation, result downloading, and JSONL result parsing. +- Added `OpenRouterBatchProviderTests` to `SmartHopper.ProviderSdk.Tests` covering `:batch` alias canonicalization, generated model metadata, registry alias resolution, and batch result parsing. +- Added batch support gating in `AIStatefulAsyncComponentBase` so a request is only routed to batch mode when the selected model declares `SupportsBatch`. +- Added centralized `AIMetrics.EstimatedCost` (USD) and `AICostCalculator` that derive estimated call cost from provider/model pricing and token buckets (input prompt, cached, cache-write, output generation, reasoning). Falls back to `EstimatedInputTokens`/`EstimatedOutputTokens` when actual token counts are zero; free, negative, or missing prices contribute `0`. +- Added `Estimated Cost` output to `Deconstruct SmartHopper Metrics` and `estimated_cost` field to metrics JSON. +- Added `SmartHopper.ProviderSdk.Tests` xUnit project with coverage for `AICostCalculator`, `AIMetrics`, `AIModelCapabilityRegistry`, `AIBody`/`AIBodyBuilder`, `AIRequestBase`/`AIRequestCall`, `AIReturn`, all `AIInteraction*` types, `AICallStatus`/`AIAgent`/`AIRequestKind`, `AICapability`/`AIModelCapabilities`, and `SHRuntimeMessage`. - Added `.github/workflows/pr-linear-history.yml` to enforce that pull requests targeting `main` (and release/hotfix branches) are rebased and contain no merge commits. - Added `.github/workflows/sync-dev-from-main.yml` to automatically rebase `dev` onto `main` and force-push; if the rebase fails, the workflow run fails so it surfaces in GitHub Actions. - Added `.github/actions/utils/rebase-onto-base` composite action to centralize rebase/base-management operations for GitHub Actions workflows. ### Changed +- `tools/Update-ProviderModels.ps1` now detects OpenRouter `:batch` model variants, canonicalizes them to the base slug, and merges `SupportsBatch`/`BatchPricing`/`Aliases` into the base model entry instead of generating separate `:batch` models. +- OpenRouter `:batch` model aliases are normalized to the base slug in `OpenRouterProvider.PreCall` so both chat and batch endpoints use the same model identifier. +- `AIStatefulAsyncComponentBase` now verifies `AIModelCapabilities.SupportsBatch` before enabling batch execution; unsupported model requests surface a runtime diagnostic. - `release-2-pr-to-dev-closed.yml` now checks `git merge-base --is-ancestor origin/main HEAD` before creating the `dev → main` release PR and rebases/force-pushes `dev` onto `main` when it is behind. - `chore-update-model-verification-template.yml` now targets the triggering branch (typically `dev`) instead of `main`, so updates flow through the regular release PR. - `chore-update-contributors.yml` and `pr-anonymize-public-key.yml` no longer delete/recreate branches and PRs; they use `peter-evans/create-pull-request` updates. - `safe-commit` and `code-style` now rebase (`git pull --rebase --autostash`) before pushing, falling back to a PR if the branch is protected. - Automated workflows that open PRs to `main`/`main-*`/`release/**`/`hotfix/**` now fetch full history and rebase onto the target base before calling `create-pull-request`. - `pr-block-dev-to-main.yml` is now `Warn Dev Release to Main`: it emits a warning and a PR comment instead of exiting with an error when a `-dev` version is merged into a protected branch. +- Updated `SmartHopper.Providers.Ollama` and `SmartHopper.Providers.LocalAI` resource icons to their official brand marks (Ollama from `@lobehub/icons`, LocalAI from the official `mudler/LocalAI` logo). ## [2.0.0-dev.260802] - 2026-08-02 diff --git a/DEV.md b/DEV.md index 850d8c589..ee553736a 100644 --- a/DEV.md +++ b/DEV.md @@ -202,8 +202,8 @@ SmartHopper currently supports the following AI providers and features: | Anthropic | ✅ Supported | Claude Console | Yes | Yes (thinking blocks) | No | Yes | Yes | Yes | No | ✅ Yes | | OpenRouter | ✅ Supported | OpenRouter | Yes | No (varies by routed model) | No | Varies | Varies | Varies | Varies | ❌ No | | Gemini | 🟠 Testing | Google AI Studio | Yes | Yes (thinking_level) | Yes | Yes | Yes | Yes | ✅ Yes | ✅ Yes | -| Ollama | ⚪ Planned | Local Ollama server | Planned | Planned | Planned | Planned | Planned | Planned | No | Planned | -| LocalAI | ⚪ Planned | LocalAI server | Planned | Planned | Planned | Planned | Planned | Planned | Planned | Planned | +| Ollama | 🟠 Testing | Local Ollama server (OpenAI-compatible) | Yes | Varies by model | Varies | Yes | Yes | Yes | No | ❌ No | +| LocalAI | 🟠 Testing | LocalAI server (OpenAI-compatible) | Yes | Varies by model | Varies | Yes | Yes | Yes | No | ❌ No | | Black Forest Labs | ⚪ Planned | Black Forest Labs API | Planned | No | No | Planned | No | No | Planned | Planned | | Stable Diffusion | ⚪ Planned | Local/API Stable Diffusion endpoint | Planned | No | No | Planned | No | No | Planned | Planned | @@ -221,7 +221,9 @@ The following table summarizes the models explicitly registered as defaults or v - `src/SmartHopper.Providers.Anthropic/AnthropicProviderModels.cs` - `src/SmartHopper.Providers.DeepSeek/DeepSeekProviderModels.cs` - `src/SmartHopper.Providers.Gemini/GeminiProviderModels.cs` +- `src/SmartHopper.Providers.LocalAI/LocalAIProviderModels.cs` - `src/SmartHopper.Providers.MistralAI/MistralAIProviderModels.cs` +- `src/SmartHopper.Providers.Ollama/OllamaProviderModels.cs` - `src/SmartHopper.Providers.OpenAI/OpenAIProviderModels.cs` - `src/SmartHopper.Providers.OpenRouter/OpenRouterProviderModels.cs` @@ -251,9 +253,9 @@ Notes: | OpenAI | `gpt-image-2-2026-04-21` | - | - | - | Image2Image | TextInput, ImageInput, ImageOutput | | OpenAI | `whisper-1` | - | - | - | Speech2Text | SpeechInput, TextOutput | | OpenRouter | `openai/gpt-5.6-luna` | - | - | - | ToolChat, ReasoningChat, ToolReasoningChat | TextInput, ImageInput, TextOutput, FunctionCalling, JsonOutput, Reasoning | -| OpenRouter | `google/gemini-3.1-flash-lite` | - | - | - | Speech2Text | TextInput, ImageInput, AudioInput, VideoInput, TextOutput, FunctionCalling, JsonOutput, Reasoning | | OpenRouter | `google/gemini-3.1-flash-lite-image` | - | - | - | Text2Image, Image2Image, Image2Text | TextInput, ImageInput, TextOutput, ImageOutput, JsonOutput, Reasoning | | OpenRouter | `google/lyria-3-pro-preview` | - | ✅ | - | Text2Speech | TextInput, ImageInput, TextOutput, AudioOutput, JsonOutput | +| OpenRouter | `google/gemini-3.1-flash-lite` | - | - | - | Speech2Text | TextInput, ImageInput, AudioInput, VideoInput, TextOutput, FunctionCalling, JsonOutput, Reasoning | | OpenRouter | `openai/gpt-5-mini` | - | ✅ | - | Text2Text, Text2Json | TextInput, ImageInput, TextOutput, FunctionCalling, JsonOutput, Reasoning | ### Discouraged models for script tools diff --git a/Directory.Build.props b/Directory.Build.props index 2ea2e25cf..75f1d15c8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,6 +9,10 @@ Copyright (C) 2024-2026 Marc Roca Musach + + true + + latest diff --git a/README.md b/README.md index 76beeec14..99ea86848 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SmartHopper - AI-Powered Tools and Assistant for Grasshopper3D -[![Version](https://img.shields.io/badge/version-2.0.0--dev.260803-brown?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases) +[![Version](https://img.shields.io/badge/version-2.0.0--dev.260825-brown?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases) [![Status](https://img.shields.io/badge/status-Unstable%20Development-brown?style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/releases) [![.NET CI](https://img.shields.io/github/actions/workflow/status/architects-toolkit/SmartHopper/.github/workflows/ci-dotnet-tests.yml?label=tests&logo=dotnet&style=for-the-badge)](https://github.com/architects-toolkit/SmartHopper/actions/workflows/ci-dotnet-tests.yml) [![Ready to use](https://img.shields.io/badge/ready_to_use-NO-brown?style=for-the-badge)](https://smarthopper.xyz/#installation) @@ -36,6 +36,8 @@ SmartHopper brings a context‑aware AI assistant and a suite of AI‑powered co - ![Anthropic](src/SmartHopper.Providers.Anthropic/Resources/anthropic_icon.png) [Anthropic](https://anthropic.com/) - ![OpenRouter](src/SmartHopper.Providers.OpenRouter/Resources/openrouter_icon.png) [OpenRouter](https://openrouter.ai/) - ![Gemini](src/SmartHopper.Providers.Gemini/Resources/gemini_icon.png) [Google Gemini](https://ai.google.dev/) + - ![LocalAI](src/SmartHopper.Providers.LocalAI/Resources/localai_icon.png) [LocalAI](https://localai.io/) — self-hosted, OpenAI-compatible + - ![Ollama](src/SmartHopper.Providers.Ollama/Resources/ollama_icon.png) [Ollama](https://ollama.com/) — local, OpenAI-compatible - Open Source — and it will always be. diff --git a/SmartHopper.sln b/SmartHopper.sln index 506d4a01c..ae89ff5eb 100644 --- a/SmartHopper.sln +++ b/SmartHopper.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 +# Visual Studio Version 18 +VisualStudioVersion = 18.8.12023.21 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Core", "src\SmartHopper.Core\SmartHopper.Core.csproj", "{D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1}" EndProject @@ -35,6 +35,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Core.Grasshoppe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Core.Tests", "src\SmartHopper.Core.Tests\SmartHopper.Core.Tests.csproj", "{C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.ProviderSdk", "src\SmartHopper.ProviderSdk\SmartHopper.ProviderSdk.csproj", "{F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.ProviderSdk.Tests", "src\SmartHopper.ProviderSdk.Tests\SmartHopper.ProviderSdk.Tests.csproj", "{65EB1E04-E30E-4708-B413-51A22FECBD9E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Providers.LocalAI", "src\SmartHopper.Providers.LocalAI\SmartHopper.Providers.LocalAI.csproj", "{CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartHopper.Providers.Ollama", "src\SmartHopper.Providers.Ollama\SmartHopper.Providers.Ollama.csproj", "{E9280136-51DA-465A-8625-5E2C064D9946}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -224,10 +232,61 @@ Global {C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x64.Build.0 = Release|Any CPU {C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x86.ActiveCfg = Release|Any CPU {C7E24C95-ADF6-4DBA-BDB7-73CFB1291053}.Release|x86.Build.0 = Release|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|x64.ActiveCfg = Debug|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|x64.Build.0 = Debug|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|x86.ActiveCfg = Debug|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Debug|x86.Build.0 = Debug|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|Any CPU.Build.0 = Release|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|x64.ActiveCfg = Release|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|x64.Build.0 = Release|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|x86.ActiveCfg = Release|Any CPU + {F59F33CC-DCD6-4EEE-9766-6E3C08E8AD43}.Release|x86.Build.0 = Release|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Debug|x64.ActiveCfg = Debug|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Debug|x64.Build.0 = Debug|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Debug|x86.ActiveCfg = Debug|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Debug|x86.Build.0 = Debug|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Release|Any CPU.Build.0 = Release|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Release|x64.ActiveCfg = Release|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Release|x64.Build.0 = Release|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Release|x86.ActiveCfg = Release|Any CPU + {65EB1E04-E30E-4708-B413-51A22FECBD9E}.Release|x86.Build.0 = Release|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Debug|x64.ActiveCfg = Debug|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Debug|x64.Build.0 = Debug|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Debug|x86.ActiveCfg = Debug|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Debug|x86.Build.0 = Debug|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Release|Any CPU.Build.0 = Release|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Release|x64.ActiveCfg = Release|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Release|x64.Build.0 = Release|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Release|x86.ActiveCfg = Release|Any CPU + {CBBB20C6-CE06-4EE7-A798-70CB83D88EB4}.Release|x86.Build.0 = Release|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Debug|x64.ActiveCfg = Debug|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Debug|x64.Build.0 = Debug|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Debug|x86.ActiveCfg = Debug|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Debug|x86.Build.0 = Debug|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Release|Any CPU.Build.0 = Release|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Release|x64.ActiveCfg = Release|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Release|x64.Build.0 = Release|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Release|x86.ActiveCfg = Release|Any CPU + {E9280136-51DA-465A-8625-5E2C064D9946}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {65EB1E04-E30E-4708-B413-51A22FECBD9E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0D5011CB-B808-41E4-A6FC-C01F3190649C} EndGlobalSection diff --git a/Solution.props b/Solution.props index c755a0263..a1c6eb140 100644 --- a/Solution.props +++ b/Solution.props @@ -1,6 +1,6 @@ - 2.0.0-dev.260803 + 2.0.0-dev.260825 \ No newline at end of file diff --git a/docs/Architecture/AIInputPayload.md b/docs/Architecture/AIInputPayload.md index 70ab00eee..3b65e9ca3 100644 --- a/docs/Architecture/AIInputPayload.md +++ b/docs/Architecture/AIInputPayload.md @@ -1,38 +1,6 @@ # AIInputPayload Wire Type Architecture -The AIInputPayload wire type enables a composable input/output architecture for SmartHopper components using input adapters, payload wires, and output components. - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core/Models/AIInputPayload.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -This document explains the composable AI input/output architecture that replaces monolithic components with small, focused adapters. Understanding this architecture is essential for building custom input adapters or output components that integrate with SmartHopper's AI pipeline. - -**You should read this if you:** - -- Want to understand how data flows between Grasshopper components and AI operations -- Are building custom input adapters or output components -- Need to understand how multiple inputs are merged before AI calls -- Want to learn about the payload rendering system for Grasshopper UI - ---- - -## End-User Guide - -### Overview +## Overview The **AIInputPayload** wire type enables a composable input/output architecture for SmartHopper components. Instead of monolithic "X→AI→Y" components, the new architecture uses: @@ -40,12 +8,23 @@ The **AIInputPayload** wire type enables a composable input/output architecture - **AIInputPayload Wire**: Carries AI interactions between components - **Output Components**: Consume payloads and execute AI operations +## Core Concepts + ### AIInputPayload A payload is a container for AI interactions with metadata: -**Payload Types:** +```csharp +public sealed class AIInputPayload +{ + public List Interactions { get; } + public AICapability InputCapabilityAtSource { get; } + public string Hint { get; } // Optional MIME type + public AIInputPayloadType PayloadType { get; } +} +``` +**Payload Types:** - `Text` — Text content interactions - `Image` — Image content interactions - `Audio` — Audio content interactions @@ -61,14 +40,11 @@ When multiple `GH_AIInputPayload` inputs are wired to the same branch path on an 3. **Result**: A single `AIBody` with all interactions in sequence Example: - ``` - Wire 0: Text("Hello") → Interaction 0 Wire 1: Text("World") → Interaction 1 Wire 2: Context("time") → Context filter added Result: AIBody with 2 text interactions + context filter "time" - ``` ### User-Readable Rendering @@ -82,20 +58,32 @@ Each payload type has a custom renderer for Grasshopper UI display: Use `AIInputPayloadRenderer.RenderToUserText(payload)` for formatted display. -### Component Integration +## Component Integration -#### Input Adapters +### Input Adapters Input adapters are **synchronous, stateless** components that: - 1. Accept Grasshopper data (text, images, files, etc.) 2. Create `AIInteraction` objects 3. Wrap in `AIInputPayload` 4. Output as `GH_AIInputPayload` Example: `Text2AI` component +```csharp +public class Text2AIComponent : GH_Component +{ + protected override void SolveInstance(IGH_DataAccess DA) + { + string text = null; + DA.GetData(0, ref text); + + var payload = AIInputPayload.FromText(text, AIAgent.User); + DA.SetData(0, new GH_AIInputPayload(payload)); + } +} +``` -#### Output Components +### Output Components Output components: @@ -105,7 +93,7 @@ Output components: 4. Execute AI call 5. Process and output results -### AIContext Component +## AIContext Component The `AIContextComponent` is a special input adapter that: @@ -120,7 +108,6 @@ The `AIContextComponent` is a special input adapter that: Example flow: ``` - AIContextComponent("time") → GH_AIInputPayload(Context("time")) ↓ [Output Component] @@ -128,12 +115,11 @@ AIContextComponent("time") → GH_AIInputPayload(Context("time")) Generates filter: "time" (or merged with other contexts) ↓ Passes to AIBodyBuilder.WithContextFilter() - ``` -### File Structure +## File Structure -#### Core Types (SmartHopper.Core/Models/) +### Core Types (SmartHopper.Core.Grasshopper/Types/) - `AIInputPayload.cs` — Core payload class and `AIInputPayloadType` enum - `GH_AIInputPayload.cs` — Grasshopper goo wrapper @@ -141,57 +127,20 @@ AIContextComponent("time") → GH_AIInputPayload(Context("time")) - `AIInputPayloadMerger.cs` — Branch-aware merging logic - `AIInputPayloadRenderer.cs` — User-readable rendering -#### Image Support (SmartHopper.Core/Models/) +### Image Support (SmartHopper.Core.Grasshopper/Types/) - `VersatileImage.cs` — Versatile image source adapter - `GH_AIImage.cs` — Grasshopper goo wrapper for images -#### Audio Support - -**SmartHopper.Core/Types/** - -- `VersatileAudio.cs` — Versatile audio source adapter (file path, URL, or byte data) -- `GH_AIAudio.cs` — Grasshopper goo wrapper for audio +### Audio Support (SmartHopper.Infrastructure/AICall/Core/Interactions/) -**SmartHopper.Infrastructure/AICall/Core/Interactions/** +- `AIInteractionAudio.cs` — Audio interaction type -- `AIInteractionAudio.cs` — Audio interaction type for AI conversations - -#### Components (SmartHopper.Components/Input/) +### Components (SmartHopper.Components/Input/) - `AIContextComponent.cs` — Context provider input adapter ---- - -## Developer Reference - -### Core Type Definition - -```csharp -public sealed class AIInputPayload -{ - public List Interactions { get; } - public AICapability InputCapabilityAtSource { get; } - public string Hint { get; } // Optional MIME type - public AIInputPayloadType PayloadType { get; } -} -``` - -### Input Adapter Example - -```csharp -public class Text2AIComponent : GH_Component -{ - protected override void SolveInstance(IGH_DataAccess DA) - { - string text = null; - DA.GetData(0, ref text); - - var payload = AIInputPayload.FromText(text, AIAgent.User); - DA.SetData(0, new GH_AIInputPayload(payload)); - } -} -``` +## Usage Examples ### Creating Text Payload @@ -206,7 +155,6 @@ var gooPayload = new GH_AIInputPayload(payload); var imageSource = VersatileImage.FromString("path/to/image.png"); var interaction = imageSource.ToInteraction(); var payload = AIInputPayload.FromImage(interaction); - ``` ### Merging Payloads @@ -223,11 +171,7 @@ var displayText = AIInputPayloadRenderer.RenderToUserText(payload); var summary = AIInputPayloadRenderer.GetSummary(payload); ``` ---- - -## Architecture & Design - -### Design Principles +## Design Principles 1. **Composability**: Small, focused input adapters can be combined flexibly 2. **Order Preservation**: Interaction order is maintained during merging @@ -235,12 +179,48 @@ var summary = AIInputPayloadRenderer.GetSummary(payload); 4. **Statelessness**: Input adapters have no internal state or AI calls 5. **Extensibility**: New payload types can be added without breaking existing code -### Current State +## Future Phases + +- **Phase 4**: Output components (`AI2Text`, `AI2List`, `AI2Json`, etc.) +- **Phase 5**: Data tree processing centralization +- **Phase 6**: Batch and sentinel centralization +- **Phase 7**: Fallback resolver (modality fallback) +- **Phase 8**: Audio provider support +- **Phase 9**: Shim wrappers and panel reorganization + +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + +--- + + +## Why Read This? -The AIInputPayload architecture is fully implemented with: +This document provides details about AIInputPayload. + + +## End-User Guide + +End-user guidance for AIInputPayload. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + +## Architecture & Design -- **Input Adapters**: Text, image, audio, and context providers -- **Output Components**: Full suite of AI2* components for converting AI responses to Grasshopper data types -- **Data Tree Processing**: Centralized through `AIInputPayloadMerger` and component integration -- **Streaming Support**: Integrated with async component base classes -- **Tool Integration**: Complete tool result envelope support with metadata tracking +Architecture and design notes for AIInputPayload. diff --git a/docs/Components/ComponentBase/AIInputAdapterBase.md b/docs/Components/ComponentBase/AIInputAdapterBase.md index 2e65b4115..86860bd2d 100644 --- a/docs/Components/ComponentBase/AIInputAdapterBase.md +++ b/docs/Components/ComponentBase/AIInputAdapterBase.md @@ -1,85 +1,78 @@ # AIInputAdapterBase +`src/SmartHopper.Core/ComponentBase/AIInputAdapterBase.cs` + Synchronous, **non-AI** base for components that build an `AIInputPayload` from a single piece of data (text, image, audio, context filter). Sits at the input edge of the AI pipeline — adapter components produced by this base feed [AIOutputAdapterBase](./AIOutputAdapterBase.md) components downstream. ---- +## Purpose -## Metadata +Provide a uniform shape for "convert X into the AI pipeline's input type" components without dragging in async/state machinery. The component always exposes a single `Input >` output of type `AIInputPayloadParameter` at index 0. -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/AIInputAdapterBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | +## Design criteria -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ +- **No AI calls.** Adapters are pure converters; they never reach a provider. +- **Single canonical output.** `RegisterOutputParams` is `sealed`; subclasses add extra outputs through `RegisterAdditionalOutputParams` starting at index 1. +- **Category locked.** Always `"SmartHopper" / "Input"`. Exposure level is per-instance via the constructor. +- **Helpers do the wrapping.** `CreateTextPayload`, `CreateImagePayload`, `CreateAudioPayload`, `CreateContextPayload` validate inputs and return `AIInputPayload`. `WrapPayload` boxes it into `GH_AIInputPayload` for the GH parameter system. ---- +## Subclass contract -## Why Read This? +```csharp +protected abstract Bitmap Icon { get; } -This base class defines the contract for converting raw data into the AI pipeline's canonical input type. If you are building a new input adapter or need to understand how data flows into the SmartHopper AI system, this documentation explains the uniform shape and constraints all input adapters must follow. +// Optional override (default: "AIInputPayload produced by this adapter."): +protected virtual string PayloadOutputDescription { get; } -**You should read this if you:** +// Optional extra outputs starting at index 1: +protected virtual void RegisterAdditionalOutputParams(GH_OutputParamManager pManager); +``` -- Are creating a new input adapter component -- Need to understand how raw data is wrapped into `AIInputPayload` -- Want to know the design constraints for the input edge of the AI pipeline +## Related ---- +- `AIInputPayload`, `AIInputPayloadParameter`, `GH_AIInputPayload` in `SmartHopper.Core`. +- [AIOutputAdapterBase](./AIOutputAdapterBase.md) — consumer side. -## End-User Guide +## Metadata -Provide a uniform shape for "convert X into the AI pipeline's input type" components without dragging in async/state machinery. The component always exposes a single `Input >` output of type `AIInputPayloadParameter` at index 0. +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach --- -## Developer Reference -### Subclass contract +## Why Read This? -```csharp -protected abstract Bitmap Icon { get; } +This document provides details about AIInputAdapterBase. -// Optional override (default: "AIInputPayload produced by this adapter."): -protected virtual string PayloadOutputDescription { get; } -// Optional extra outputs starting at index 1: -protected virtual void RegisterAdditionalOutputParams(GH_OutputParamManager pManager); +## End-User Guide -``` +End-user guidance for AIInputAdapterBase. -### Helper methods -```csharp -// Helpers do the wrapping: -// CreateTextPayload, CreateImagePayload, CreateAudioPayload, CreateContextPayload -// validate inputs and return AIInputPayload. -// WrapPayload boxes it into GH_AIInputPayload for the GH parameter system. +## Developer Reference -// Example usage inside a derived adapter: -protected override void SolveInstance(IGH_DataAccess DA) -{ - string text = ""; - if (!DA.GetData(0, ref text)) return; +Example usage: - var payload = CreateTextPayload(text); - DA.SetData(0, WrapPayload(payload)); -} +`csharp +// Placeholder example +``r -``` +`csharp +// Another placeholder example +``r ---- ## Architecture & Design -- **No AI calls.** Adapters are pure converters; they never reach a provider. -- **Single canonical output.** `RegisterOutputParams` is `sealed`; subclasses add extra outputs through `RegisterAdditionalOutputParams` starting at index 1. -- **Category locked.** Always `"SmartHopper" / "B. Input"`. Exposure level is per-instance via the constructor. -- **Helpers do the wrapping.** `CreateTextPayload`, `CreateImagePayload`, `CreateAudioPayload`, `CreateContextPayload` validate inputs and return `AIInputPayload`. `WrapPayload` boxes it into `GH_AIInputPayload` for the GH parameter system. +Architecture and design notes for AIInputAdapterBase. -### Related +```csharp +// Example code for Developer Reference +``` -- `AIInputPayload`, `AIInputPayloadParameter`, `GH_AIInputPayload` in `SmartHopper.Core`. -- [AIOutputAdapterBase](./AIOutputAdapterBase.md) — consumer side. +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/AIOutputAdapterBase.md b/docs/Components/ComponentBase/AIOutputAdapterBase.md index edd3d5357..937bed74e 100644 --- a/docs/Components/ComponentBase/AIOutputAdapterBase.md +++ b/docs/Components/ComponentBase/AIOutputAdapterBase.md @@ -1,43 +1,24 @@ # AIOutputAdapterBase -Specialised [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) for components that consume `AIInputPayload` trees, send them to the AI through a forced tool call, and project the response onto one or more typed outputs (e.g. `AI2Boolean`, `AI2Number`, `AI2GhJson`, …). - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/AIOutputAdapterBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | +`src/SmartHopper.Core/ComponentBase/AIOutputAdapterBase.cs` -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -This base class eliminates boilerplate for components that turn AI payload trees into typed Grasshopper outputs. If you are building a new "AI2X" style component or need to understand how the adapter pattern maps AI responses to parameters, this is the authoritative reference. - -**You should read this if you:** - -- Are creating a new output adapter component (e.g. `AI2Boolean`, `AI2Number`) -- Need to understand how `OutputMapping` declaratively drives parameter generation -- Want to know how branch-by-branch execution and batch mode work for output adapters - ---- +Specialised [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) for components that consume `AIInputPayload` trees, send them to the AI through a forced tool call, and project the response onto one or more typed outputs (e.g. `AI2Boolean`, `AI2Number`, `AI2GhJson`, …). -## End-User Guide +## Purpose Eliminate boilerplate for "tree of payloads in → typed values out" components. The base handles parameter shape, branch-by-branch execution, batch interception, decoding via declarative output mappings and atomic finalization. ---- +## Design criteria -## Developer Reference +- **Branch-by-branch conversation.** `ComponentProcessingOptions` is fixed to `BranchToBranch`: each input branch is one merged `AIBody` → one provider call. +- **Forced tool calling.** `CallAIAsync(body, forceToolName: ...)` is used so the model is constrained to call the adapter's tool. Capabilities are derived from `UsingAiTools` (no need to override `RequiredCapability`). +- **Declarative outputs via `OutputMapping`.** Subclasses declare a list; the base auto-registers Grasshopper outputs and runs every mapping's `Extractor` against the `AIReturn`. The first mapping is the *primary* output for batch reconstruction. +- **`OutputMapping.Single` helper** wraps a scalar extractor into the unified `IEnumerable` contract; list-shaped extractors return their list. +- **Symmetric batch and sync paths.** Both legs run the same `DecodeAllMappings` helper, so list-shaped outputs work transparently in batch mode. The legacy `SentinelTransformOutputs` hook is **not** invoked by the adapter base — declare every named output through `GetOutputMappings` instead. +- **Sealed input shape.** Adds `Input >` (`AIInputPayloadParameter`, tree access) at index 0, then chains to `base.RegisterInputParams`. Subclasses use `RegisterAdditionalInputParams` for extra inputs and `GatherAdditionalInputs(DA, dict)` to inject them into the per-branch input dictionary. +- **Category locked** to `"SmartHopper" / "Output"`. -### Subclass contract +## Subclass contract ```csharp protected abstract string GetInternalSystemPrompt(); @@ -48,10 +29,9 @@ protected abstract IReadOnlyList GetOutputMappings(); // must protected virtual void GatherAdditionalInputs(IGH_DataAccess DA, Dictionary additionalInputs); protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager); protected override IReadOnlyList UsingAiTools => new[] { "..." }; - ``` -### `OutputMapping` +## `OutputMapping` ```csharp public class OutputMapping @@ -65,38 +45,57 @@ public class OutputMapping public static Func> Single(Func extractor); } +``` + +## Execution +```text +PrepareInputs (merge Input> branch into AIBody, prepend system prompt) + → CallAIAsync(body, forceToolName) + → DecodeAllMappings(AIReturn) ← runs every Extractor + → FinishResults(primary, …extras) ← atomic persistence + metrics ``` -### Execution flow +In batch mode the call is queued, `OnBatchCompleted` invokes `ProcessMappingsBatchResults` which calls the same `DecodeAllMappings` helper before delegating to `FinishResults`. -```csharp -// Conceptual execution steps inside the adapter base: -// 1. PrepareInputs merges the Input> branch into an AIBody and prepends the system prompt. -// 2. CallAIAsync(body, forceToolName) sends the request. -// 3. DecodeAllMappings(AIReturn) runs every Extractor for each registered OutputMapping. -// 4. FinishResults(primary, …extras) handles atomic persistence and metrics. +## Related -// In batch mode: -// - The call is queued via OnBatchCompleted. -// - ProcessMappingsBatchResults calls the same DecodeAllMappings helper. -// - Then delegates to FinishResults for persistence. +- [AIInputAdapterBase](./AIInputAdapterBase.md) — producer side. +- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) — execution model and metrics. -``` +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach --- -## Architecture & Design -- **Branch-by-branch conversation.** `ComponentProcessingOptions` is fixed to `BranchToBranch`: each input branch is one merged `AIBody` → one provider call. -- **Forced tool calling.** `CallAIAsync(body, forceToolName: ...)` is used so the model is constrained to call the adapter's tool. Capabilities are derived from `UsingAiTools` (no need to override `RequiredCapability`). -- **Declarative outputs via `OutputMapping`.** Subclasses declare a list; the base auto-registers Grasshopper outputs and runs every mapping's `Extractor` against the `AIReturn`. The first mapping is the *primary* output for batch reconstruction. -- **`OutputMapping.Single` helper** wraps a scalar extractor into the unified `IEnumerable` contract; list-shaped extractors return their list. -- **Symmetric batch and sync paths.** Both legs run the same `DecodeAllMappings` helper, so list-shaped outputs work transparently in batch mode. The legacy `SentinelTransformOutputs` hook is **not** invoked by the adapter base — declare every named output through `GetOutputMappings` instead. -- **Sealed input shape.** Adds `Input >` (`AIInputPayloadParameter`, tree access) at index 0, then chains to `base.RegisterInputParams`. Subclasses use `RegisterAdditionalInputParams` for extra inputs and `GatherAdditionalInputs(DA, dict)` to inject them into the per-branch input dictionary. -- **Category locked** to `"SmartHopper" / "C. Output"`. +## Why Read This? -### Related +This document provides details about AIOutputAdapterBase. -- [AIInputAdapterBase](./AIInputAdapterBase.md) — producer side. -- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) — execution model and metrics. + +## End-User Guide + +End-user guidance for AIOutputAdapterBase. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + +## Architecture & Design + +Architecture and design notes for AIOutputAdapterBase. diff --git a/docs/Components/ComponentBase/AIProviderComponentBase.md b/docs/Components/ComponentBase/AIProviderComponentBase.md index 701ac2619..26b6619d2 100644 --- a/docs/Components/ComponentBase/AIProviderComponentBase.md +++ b/docs/Components/ComponentBase/AIProviderComponentBase.md @@ -1,84 +1,83 @@ # AIProviderComponentBase -Async + stateful base that adds an AI provider selection menu to [StatefulComponentBase](./StatefulComponentBase.md). Implements [`IProviderComponent`](./index.md). Delegates the menu and serialization to [ProviderSelectionCore](./ProviderSelectionCore.md). +`src/SmartHopper.Core/ComponentBase/AIProviderComponentBase.cs` + +Async + stateful base that adds an AI provider selection menu to [StatefulComponentBase](./StatefulComponentBase.md). Implements [`IProviderComponent`](./index.md). Delegates the menu and serialization to [ProviderComponentHelper](./ProviderComponentHelper.md). --- -## Metadata +Let the user pick an AI provider per component (or fall back to the system default), persist the choice across save/load, and expose the resolved provider to derived classes. -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/AIProviderComponentBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | +## Design criteria -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ +- **`"Default"` is a sentinel.** Stored as the literal string `"Default"` (`ProviderComponentHelper.DEFAULT_PROVIDER`); resolved at use-time through `ProviderManager.GetDefaultAIProvider()`. This keeps documents portable across machines with different default providers. +- **Provider change is a real input change.** `InputsChanged()` is overridden to add `"AIProvider"` to the list whenever `HasProviderChanged()` is true, so the state machine treats it like any other input change. `StatefulComponentBase` further forces `NeedsRun` for that input. +- **Custom attributes.** `CreateAttributes()` installs `AIProviderComponentAttributes`, which renders the provider logo and the small status badge. Derived classes that need a richer overlay (e.g. AI components with model badges) override this. ---- +## Key members -## Why Read This? +- `string SelectedProviderName` — the literal stored value (may be `"Default"`). +- `string GetActualAIProviderName()` — resolves `"Default"` through `ProviderManager`. +- `AIProvider GetActualAIProvider()` — the concrete provider instance, or `null`. +- `bool HasProviderChanged()` — true exactly once after the user changed the selection (clears the previous-selection cache as a side effect). +- `void SetSelectedProviderName(string name)`. -This base class is the foundation for any component that needs an AI provider selection menu, persisted across save/load, without the full AI request orchestration stack. If you are building a custom component that needs provider selection, this documentation explains the contract and persistence model. +## Persistence -**You should read this if you:** +Provider name is written through `ProviderComponentHelper.WriteProvider/ReadProvider` under the `"AIProvider"` key. If a saved provider is no longer available, the read silently falls back to `"Default"`. -- Are building a new component that requires AI provider selection -- Need to understand how provider names are persisted and resolved -- Want to know how provider changes trigger the state machine re-execution +## When to derive ---- +- You need provider selection but **not** the AI request orchestration (Settings input, Metrics output, badges, batch). For those use [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md). +- For non-async provider components (e.g. `AIModels` listing) use [ProviderComponentBase](./ProviderComponentBase.md). -## End-User Guide +This base class is the foundation for any component that needs an AI provider selection menu, persisted across save/load, without the full AI request orchestration stack. If you are building a custom component that needs provider selection, this documentation explains the contract and persistence model. -Let the user pick an AI provider per component (or fall back to the system default), persist the choice across save/load, and expose the resolved provider to derived classes. +- [ProviderComponentBase](./ProviderComponentBase.md) +- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) +- `IProviderComponent`, `ProviderComponentHelper`, `AIProviderComponentAttributes` -### When to derive +## Metadata -- You need provider selection but **not** the AI request orchestration (Settings input, Metrics output, badges, batch). For those use [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md). -- For non-async provider components (e.g. `AIModels` listing) use [ProviderComponentBase](./ProviderComponentBase.md). +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach --- -## Developer Reference -### Key members +## Why Read This? -```csharp -string SelectedProviderName { get; } // the literal stored value (may be "Default") -string GetActualAIProviderName(); // resolves "Default" through ProviderManager -AIProvider GetActualAIProvider(); // the concrete provider instance, or null -bool HasProviderChanged(); // true exactly once after user changed selection -void SetSelectedProviderName(string name); +This document provides details about AIProviderComponentBase. -``` -### Persistence +## End-User Guide + +End-user guidance for AIProviderComponentBase. -```csharp -// Provider name is written through ProviderSelectionCore under the "AIProvider" key: -// WriteProvider(writer, SelectedProviderName) -// ReadProvider(reader) -> returns the stored name or "Default" if unavailable -// Example override in a derived component: -protected override void WriteAdditionalData(GH_IWriter writer) -{ - base.WriteAdditionalData(writer); - // ProviderSelectionCore handles the actual "AIProvider" key serialization -} +## Developer Reference -``` +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r ---- ## Architecture & Design -- **`"Default"` is a sentinel.** Stored as the literal string `"Default"` (`ProviderSelectionCore.DEFAULT_PROVIDER`); resolved at use-time through `ProviderManager.GetDefaultAIProvider()`. This keeps documents portable across machines with different default providers. -- **Provider change is a real input change.** `InputsChanged()` is overridden to add `"AIProvider"` to the list whenever `HasProviderChanged()` is true, so the state machine treats it like any other input change. `StatefulComponentBase` further forces `NeedsRun` for that input. -- **Custom attributes.** `CreateAttributes()` installs `AIProviderComponentAttributes`, which renders the provider logo and the small status badge. Derived classes that need a richer overlay (e.g. AI components with model badges) override this. +Architecture and design notes for AIProviderComponentBase. -### Related +```csharp +// Example code for Developer Reference +``` -- [ProviderComponentBase](./ProviderComponentBase.md) -- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) -- `IProviderComponent`, `ProviderSelectionCore`, `AIProviderComponentAttributes` +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/AISelectingStatefulAsyncComponentBase.md b/docs/Components/ComponentBase/AISelectingStatefulAsyncComponentBase.md index 77118ff00..f8c9e199e 100644 --- a/docs/Components/ComponentBase/AISelectingStatefulAsyncComponentBase.md +++ b/docs/Components/ComponentBase/AISelectingStatefulAsyncComponentBase.md @@ -1,92 +1,80 @@ # AISelectingStatefulAsyncComponentBase +`src/SmartHopper.Core/ComponentBase/AISelectingStatefulAsyncComponentBase.cs` + Extends [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) with the canvas selection UI. Implements `ISelectingComponent`. Delegates selection to the shared `SelectingComponentCore` helper. ---- +## Purpose -## Metadata +AI components that need the user to pick other Grasshopper objects on the canvas as part of their input — for example *Smart Connect* and other canvas-aware AI utilities. -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/AISelectingStatefulAsyncComponentBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | +## What it adds -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ +- Constructor creates a `SelectingComponentCore` and subscribes to document events. +- `CreateAttributes` installs `AISelectingComponentAttributes`, which extends [`ComponentBadgesAttributes`](./AIStatefulAsyncComponentBase.md) so badges (provider, model) coexist with the Select button. The provider tooltip is rendered last so it stays above the Select overlay. +- Adds a *Select Components* item to the context menu. +- `Write` / `Read` chain into `SelectingComponentCore` for GUID-based persistence on top of all the AI persistence (batch state, sentinels, hashes, outputs). +- `RemovedFromDocument` unsubscribes the selection core from document events. ---- +## Public selection API -## Why Read This? +```csharp +public List SelectedObjects { get; } +public void EnableSelectionMode(); +``` -This base class bridges AI-driven Grasshopper components with interactive canvas selection, allowing users to pick objects that become part of the AI workflow. It is essential for any SmartHopper component that needs to reference other document objects. +`SelectedObjects` is auto-pruned of deleted references on every read. -**You should read this if you:** +## Design criteria -- Are building an AI component that needs to pick other Grasshopper objects on the canvas -- Want to understand how selection state is persisted alongside AI batch state -- Need to know how the Select button cooperates with badge rendering +Same as [SelectingComponentBase](./SelectingComponentBase.md) — selection logic is shared via `SelectingComponentCore`, GUIDs are persisted, UI work runs on Rhino's UI thread. The AI base contributes nothing new to the selection pipeline; it only ensures the Select button cooperates with badge rendering. ---- +## Related -## End-User Guide +- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) +- [SelectingComponentBase](./SelectingComponentBase.md) -### Purpose +## Metadata -AI components that need the user to pick other Grasshopper objects on the canvas as part of their input — for example *Smart Connect* and other canvas-aware AI utilities. +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach -### What it adds +--- -- Constructor creates a `SelectingComponentCore` and subscribes to document events. -- `CreateAttributes` installs `AISelectingComponentAttributes`, which extends [`ComponentBadgesAttributes`](./AIStatefulAsyncComponentBase.md) so badges (provider, model) coexist with the Select button. The provider tooltip is rendered last so it stays above the Select overlay. -- Adds a *Select Components* item to the context menu. -- `Write` / `Read` chain into `SelectingComponentCore` for GUID-based persistence on top of all the AI persistence (batch state, sentinels, hashes, outputs). -- `RemovedFromDocument` unsubscribes the selection core from document events. ---- +## Why Read This? -## Developer Reference +This document provides details about AISelectingStatefulAsyncComponentBase. -### Public selection API -```csharp -public List SelectedObjects { get; } -public void EnableSelectionMode(); +## End-User Guide -``` +End-user guidance for AISelectingStatefulAsyncComponentBase. -`SelectedObjects` is auto-pruned of deleted references on every read. -### Example: implementing a selecting AI component +## Developer Reference -```csharp -public class MySelectingComponent : AISelectingStatefulAsyncComponentBase -{ - public MySelectingComponent() - : base("MySelecting", "MSC", "An AI component that selects objects") - { - } - - protected override void OnSolveInstancePostSolve(IGH_DataAccess DA) - { - // Access user-selected objects from the canvas - var selected = this.SelectedObjects; - // ... - base.OnSolveInstancePostSolve(DA); - } -} +Example usage: -``` +`csharp +// Placeholder example +``r ---- +`csharp +// Another placeholder example +``r -## Architecture & Design -### Design criteria +## Architecture & Design -Same as [SelectingComponentBase](./SelectingComponentBase.md) — selection logic is shared via `SelectingComponentCore`, GUIDs are persisted, UI work runs on Rhino's UI thread. The AI base contributes nothing new to the selection pipeline; it only ensures the Select button cooperates with badge rendering. +Architecture and design notes for AISelectingStatefulAsyncComponentBase. -### Related +```csharp +// Example code for Developer Reference +``` -- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) -- [SelectingComponentBase](./SelectingComponentBase.md) +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/AIStatefulAsyncComponentBase.md b/docs/Components/ComponentBase/AIStatefulAsyncComponentBase.md index b30af6057..e616f5a48 100644 --- a/docs/Components/ComponentBase/AIStatefulAsyncComponentBase.md +++ b/docs/Components/ComponentBase/AIStatefulAsyncComponentBase.md @@ -1,37 +1,5 @@ # AIStatefulAsyncComponentBase -The canonical base for any component that talks to an AI provider through SmartHopper. It owns the provider/model resolution, the AI request lifecycle (sync or batch), the metrics emission and the on-canvas badge rendering, so derived components only need to express *what* they do (system prompt, tool, output mapping). - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/AIStatefulAsyncComponentBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -This is the most important base class for all AI-driven SmartHopper components. It handles everything from model selection and request parameters to batch processing and metrics tracking, freeing component authors to focus on their specific AI task. - -**You should read this if you:** - -- Are creating a new AI component in SmartHopper -- Need to understand the batch processing lifecycle -- Want to customize model selection, metrics, or output transformation -- Need to implement capability-aware AI tool usage - ---- - -## End-User Guide - `src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.*.cs` — partial class split across **8 files**: | File | Concern | @@ -47,34 +15,33 @@ This is the most important base class for all AI-driven SmartHopper components. Inherits from [AIProviderComponentBase](./AIProviderComponentBase.md). Adds a `Settings` input (parameters, model, extras) and a `Metrics` output, AI request orchestration, capability-aware model selection, badge rendering and a full batch-processing pipeline. -### Purpose +--- The canonical base for any component that talks to an AI provider through SmartHopper. It owns the provider/model resolution, the AI request lifecycle (sync or batch), the metrics emission and the on-canvas badge rendering, so derived components only need to express *what* they do (system prompt, tool, output mapping). -### Inputs / outputs added +## Inputs / outputs added - `Settings` (`S`, `IGH_Goo`, optional, item) — accepts an `AIRequestParameters` (preferred) or any value cast to a model name string. Drives the `_requestParameters` field used for model, temperature, max tokens, batch tier, custom timeout and provider-specific extras. - `Run?` (`R`, bool, item, default `false`) — inherited from `StatefulComponentBase`. -- `Metrics` (`M`, text, item) — JSON containing provider/model, all token counters, finish reason, completion time, context usage, data count and iteration count. See `SetMetricsOutput`. When processing multiple branches the payload is a JSON array with one entry per branch so downstream components (e.g. `Deconstruct Metrics`) can expand it into parallel lists. +- `Metrics` (`M`, text, item) — JSON containing provider/model, all token counters, finish reason, completion time, context usage, data count and iteration count. See `SetMetricsOutput`. -### Capability-aware model selection +## Capability-aware model selection - `protected virtual AICapability RequiredCapability { get; set; }` — the component's declared capability. The getter merges in capabilities required by every tool listed in `UsingAiTools`. - `protected virtual IReadOnlyList UsingAiTools => Array.Empty()` — names of tools used by this component. Drives the merged capability and the *not recommended* badge. - `GetModel()` returns the user's model from `Settings`, or the provider's `GetDefaultModel(RequiredCapability, useSettings: true)` fallback. - `UpdateBadgeCache()` runs a synthetic `AIRequestCall` through validation to populate the badge flags: *Verified*, *Deprecated*, *Invalid*, *Replaced*, *Not recommended*. Cached per solve. -### Two execution modes +## Two execution modes Both modes converge at `FinishResults` and emit metrics atomically. -#### Non-batch (sync) +### Non-batch (sync) ```text DoWorkAsync() └── RunProcessingAsync() [via DataTreeProcessor.RunAsync] └── function(inputs) [once per branch/item] - 1. PrepareInputs(inputs, ctx) ← virtual hook 2. CallAIToolAsync(...) / CallAIAsync(...) 3. (return outputs dict) @@ -82,16 +49,14 @@ DoWorkAsync() → SetPersistentOutput per output → SetMetricsOutput(null) └── Worker.SetOutput() → no-op - ``` -#### Batch +### Batch ```text DoWorkAsync() └── RunProcessingAsync() └── function(inputs) - 1. PrepareInputs(inputs, ctx) 2. CallAIToolAsync / CallAIAsync queues item, returns sentinel ##SH_BATCH:{customId}## └── TrySubmitBatchAsync() → submits queue to provider @@ -106,47 +71,21 @@ PollBatchStatusAsync() [background timer] SetAIReturnSnapshot(aggregatedMetrics) FinishResults(primary, ...extras) [Completed] → ExpireSolution → RestorePersistentOutputs - ``` -### Metrics - -Two emission patterns: - -| Component shape | Where metrics are emitted | -| --- | --- | -| Standard AI components | `FinishResults` (non-batch) or `ProcessBatchResults` → `FinishResults` (batch). | -| Sync-output components (e.g. `AIChatComponent`) | Call `SetMetricsOutput(DA)` directly inside their `SolveInstance`; do not use `FinishResults`. | - -When the component processes multiple branches, emit one metric entry per branch. Non-batch workers accumulate entries via `CombineIntoPersistedMetrics`; batch workers collect them automatically in `ProcessBatchResults`. The resulting `AIMetricsList` is serialized as a single JSON object when all entries share the same provider/model, or as a JSON array when they differ (e.g. after modality fallback). - -`OnSolveInstancePostSolve` does not call `SetMetricsOutput`. The single authoritative metrics instance is `PersistedMetricsList`; `AIReturn.Metrics` re-aggregates on every read so writes to it are no-ops, hence the dedicated field. - -### Batch features - -- `IsBatchRequest()` — true when `Settings.BatchTier == true` **and** the provider implements `IAIBatchProvider`. Surfaces a one-shot remark when batch is requested but unsupported. -- `TrySubmitBatchAsync(outputName, resultDict, ct)` — convenience: stores the sentinel tree and submits the queue. -- `_sentinelTrees`, `_batchSentinelIds`, `_batchQueue`, `_batchSubmission` — explicit lifecycle, see XML docs in `Main.cs`. Persisted by `Write/Read` so polling resumes after file save/load and *Load results from file* can rebuild trees. -- Cancellation: `OnTasksCancelDetected` immediately surfaces *"Cancelling batch ..."*, calls `IAIBatchProvider.CancelBatchAsync`, then updates the message with success/failure. -- Context menu: *Load results from file* (multi-select) and *Check batch status* (enabled only while a batch is active). - ---- - -## Developer Reference - -### Virtual hooks +## Virtual hooks -#### `PrepareInputs(Dictionary inputs, ProcessingUnitContext ctx)` +### `PrepareInputs(Dictionary inputs, ProcessingUnitContext ctx)` Fires inside `function(inputs)` in both modes, **before** the AI call. Override to inject derived fields, normalize values or validate early. Inputs are not available to `SentinelTransformOutputs` in batch mode — cache anything you need on the component. -#### `SentinelTransformOutputs(Dictionary decodedOutputs, ProcessingUnitContext ctx)` +### `SentinelTransformOutputs(Dictionary decodedOutputs, ProcessingUnitContext ctx)` Fires after each result is decoded, before `FinishResults`. Default returns the dictionary unchanged. Override to split one response into multiple outputs or to add computed fields. Extra keys (anything other than the primary output param name) are accumulated and forwarded to `FinishResults` as `additionalOutputs`. > Renamed from the historical `TransformOutputs`. The previous name no longer exists. -#### `ProcessingUnitContext` +### `ProcessingUnitContext` ```csharp public readonly struct ProcessingUnitContext @@ -155,10 +94,9 @@ public readonly struct ProcessingUnitContext public int? ItemIndex { get; init; } // non-batch only public string SentinelId { get; init; } // batch only } - ``` -### `FinishResults` +## `FinishResults` ```csharp protected void FinishResults( @@ -166,7 +104,6 @@ protected void FinishResults( GH_Structure primaryTree, params (string name, object value)[] additionalOutputs) where T : IGH_Goo; - ``` Single finalization point invoked by both modes. Routing for `additionalOutputs.value`: @@ -177,7 +114,26 @@ Single finalization point invoked by both modes. Routing for `additionalOutputs. It also stamps `CompletionTime` from `_batchCompletionTime` when present and emits the `Metrics` output via `SetMetricsOutput(null)`. -### Implementation requirements (standard AI component) +## Metrics + +Two emission patterns: + +| Component shape | Where metrics are emitted | +| --- | --- | +| Standard AI components | `FinishResults` (non-batch) or `ProcessBatchResults` → `FinishResults` (batch). | +| Sync-output components (e.g. `AIChatComponent`) | Call `SetMetricsOutput(DA)` directly inside their `SolveInstance`; do not use `FinishResults`. | + +`OnSolveInstancePostSolve` does not call `SetMetricsOutput`. The single authoritative metrics instance is `_persistedMetrics`; `AIReturn.Metrics` re-aggregates on every read so writes to it are no-ops, hence the dedicated field. + +## Batch features + +- `IsBatchRequest()` — true when `Settings.BatchTier == true` **and** the provider implements `IAIBatchProvider`. Surfaces a one-shot remark when batch is requested but unsupported. +- `TrySubmitBatchAsync(outputName, resultDict, ct)` — convenience: stores the sentinel tree and submits the queue. +- `_sentinelTrees`, `_batchSentinelIds`, `_batchQueue`, `_batchSubmission` — explicit lifecycle, see XML docs in `Main.cs`. Persisted by `Write/Read` so polling resumes after file save/load and *Load results from file* can rebuild trees. +- Cancellation: `OnTasksCancelDetected` immediately surfaces *"Cancelling batch …"*, calls `IAIBatchProvider.CancelBatchAsync`, then updates the message with success/failure. +- Context menu: *Load results from file* (multi-select) and *Check batch status* (enabled only while a batch is active). + +## Implementation requirements (standard AI component) ```csharp // Worker.DoWorkAsync — non-batch branch must call FinishResults: @@ -203,22 +159,65 @@ protected override void OnBatchCompleted( (customId, body) => new GH_String(Decode(body)), messages); } +``` + +For multi-output components, pass extras to `FinishResults`: +```csharp +parent.FinishResults( + "Markdown", markdownTree, + ("Images", imagesTree), + ("Format", formatTree)); ``` -### Context menu overrides +## Usage checklist -- `AppendAdditionalComponentMenuItems` adds *Load results from file* and *Check batch status*. -- `Menu_LoadResultsFromFile_Click` opens a file dialog and feeds files into `ProcessBatchResults`. -- `Menu_CheckBatchStatus_Click` forces an immediate poll. +- Override `RequiredCapability` (and `UsingAiTools` if the component routes through tools). +- Use `CallAIToolAsync` for tool-based work and `CallAIAsync` for full chat-completion calls (e.g. output adapters with forced tool calls). +- Override `PrepareInputs` / `SentinelTransformOutputs` for input/output shaping. +- Implement `OnBatchCompleted` and call `ProcessBatchResults` if your component supports batch. +- Use `SetPersistentRuntimeMessage` (inherited) for errors that should persist across solves. +- Use `SetAIReturnSnapshot(ret)` if an external source (e.g. WebChat) provides the response. + +This is the most important base class for all AI-driven SmartHopper components. It handles everything from model selection and request parameters to batch processing and metrics tracking, freeing component authors to focus on their specific AI task. + +- [AIProviderComponentBase](./AIProviderComponentBase.md), [StatefulComponentBase](./StatefulComponentBase.md) +- [AIOutputAdapterBase](./AIOutputAdapterBase.md), [AISelectingStatefulAsyncComponentBase](./AISelectingStatefulAsyncComponentBase.md) +- [BatchSentinel](./BatchSentinel.md), [Data tree processing schema](./DataTreeProcessingSchema.md) + +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach --- -## Architecture & Design -### Related +## Why Read This? -- [AIProviderComponentBase](./AIProviderComponentBase.md) -- [AISelectingStatefulAsyncComponentBase](./AISelectingStatefulAsyncComponentBase.md) -- [AsyncComponentBase](./AsyncComponentBase.md), [AsyncWorkerBase](./AsyncWorkerBase.md) -- [BatchSentinel](./BatchSentinel.md), [Data tree processing schema](./DataTreeProcessingSchema.md) +This document provides details about AIStatefulAsyncComponentBase. + + +## End-User Guide + +End-user guidance for AIStatefulAsyncComponentBase. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + +## Architecture & Design + +Architecture and design notes for AIStatefulAsyncComponentBase. diff --git a/docs/Components/ComponentBase/AsyncComponentBase.md b/docs/Components/ComponentBase/AsyncComponentBase.md index ec22daa55..cc26b32bc 100644 --- a/docs/Components/ComponentBase/AsyncComponentBase.md +++ b/docs/Components/ComponentBase/AsyncComponentBase.md @@ -1,45 +1,22 @@ # AsyncComponentBase -Base class for Grasshopper components that run long-running work on a background `Task`. Inherits from `GH_Component`. Adapted from Speckle's `GrasshopperAsyncComponent`. - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/AsyncComponentBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- +`src/SmartHopper.Core/ComponentBase/AsyncComponentBase.cs` -## Why Read This? - -This base class enables Grasshopper components to execute heavy or I/O-bound operations without freezing the Rhino UI. Understanding its two-phase solve and worker model is essential for building responsive asynchronous components. - -**You should read this if you:** - -- Need to run compute-heavy or I/O-bound work off the Grasshopper UI thread -- Want to understand the pre-solve / post-solve handshake and output-phase mechanics -- Are deriving from [StatefulComponentBase](./StatefulComponentBase.md) and want to understand the underlying async machinery +Base class for Grasshopper components that run long-running work on a background `Task`. Inherits from `GH_Component`. Adapted from Speckle's `GrasshopperAsyncComponent`. --- -## End-User Guide - -### Purpose - Run compute-heavy or I/O-bound work off the UI thread while staying inside Grasshopper's solve lifecycle. Provides cancellation, exception propagation and a strict pre-/post-solve handshake so outputs are written exactly once per run. ---- +## Design criteria -## Developer Reference +- **Two-phase solve.** Pre-solve gathers inputs and starts the task; post-solve consumes the result. Tracked by `_state` (worker completion counter) and `_setData` (output-phase latch). +- **Workers, not tasks.** Compute logic lives in an `AsyncWorkerBase` returned from `CreateWorker(progressReporter)`. The component owns the lifecycle; the worker owns the compute. +- **LIFO output ordering.** Workers are reversed before output to match Grasshopper's expected ordering when multiple workers are present. +- **UI thread safety.** `SetOutput` is invoked through `Rhino.RhinoApp.InvokeOnUiThread`. Workers must never call GH/Rhino UI directly. +- **Cancellation is first-class.** Each task gets a dedicated `CancellationTokenSource`. The context-menu item *Cancel current process* and `RequestTaskCancellation()` cancel all sources; the cancellation is detected in the continuation, which raises `OnTasksCancelDetected()`. -### Key members +## Key members - `protected abstract AsyncWorkerBase CreateWorker(Action progressReporter)` — factory. - `protected virtual void OnSolveInstancePreSolve(IGH_DataAccess DA)` / `OnSolveInstancePostSolve(IGH_DataAccess DA)` — hooks. @@ -50,63 +27,60 @@ Run compute-heavy or I/O-bound work off the UI thread while staying inside Grass - `protected int DataCount { get; }` / `SetDataCount(int)` — surfaces the data count for state messages and metrics. - `public bool InPreSolve` — exposed for `IGH_TaskCapableComponent` callers. -### Example: overriding the solve lifecycle hooks +## Lifecycle -```csharp -public class MyAsyncComponent : AsyncComponentBase -{ - protected override void OnSolveInstancePreSolve(IGH_DataAccess DA) - { - // Gather any pre-solve state or validate inputs here - base.OnSolveInstancePreSolve(DA); - } - - protected override void OnSolveInstancePostSolve(IGH_DataAccess DA) - { - // Perform any post-processing after workers have set outputs - base.OnSolveInstancePostSolve(DA); - } -} +1. **`BeforeSolveInstance`** – cancels in-flight tasks (unless we are already in the output phase) and calls `ResetAsyncState`. +2. **`SolveInstance` (pre-solve)** – first pass: creates a worker, calls `worker.GatherInput`, starts `Task.Run(worker.DoWorkAsync)`, then returns so Grasshopper proceeds to `AfterSolveInstance`. +3. **`AfterSolveInstance`** – `Task.WhenAll` of all worker tasks. On success: sets `_state = Workers.Count`, `_setData = 1`, reverses workers (LIFO). On cancel: resets state, raises `OnTasksCancelDetected`. On fault: surfaces task errors as runtime messages and still proceeds to the output phase. Always re-expires the solution to drive the second solve pass. +4. **`SolveInstance` (post-solve)** – second pass: invokes `worker.SetOutput` for each worker on the UI thread, decrements `_state`. When `_state == 0` clears tasks/workers/sources and calls `OnWorkerCompleted`. -``` +## When to derive -### Example: requesting cancellation programmatically +- You need async work but **do not** need a state machine, debounce or `Run` button semantics. For all of those use [StatefulComponentBase](./StatefulComponentBase.md). +- Keep mutable state out of the worker; pass an immutable input snapshot in `GatherInput`. -```csharp -public override void AppendAdditionalComponentMenuItems(ToolStripDropDown menu) -{ - base.AppendAdditionalComponentMenuItems(menu); - Menu_AppendItem(menu, "Abort", (s, e) => this.RequestTaskCancellation()); -} +## End-User Guide -``` +- [AsyncWorkerBase](./AsyncWorkerBase.md) +- [StatefulComponentBase](./StatefulComponentBase.md) +- [ProgressInfo](./ProgressInfo.md) + +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach --- -## Architecture & Design -### Design criteria +## Why Read This? -- **Two-phase solve.** Pre-solve gathers inputs and starts the task; post-solve consumes the result. Tracked by `_state` (worker completion counter) and `_setData` (output-phase latch). -- **Workers, not tasks.** Compute logic lives in an `AsyncWorkerBase` returned from `CreateWorker(progressReporter)`. The component owns the lifecycle; the worker owns the compute. -- **LIFO output ordering.** Workers are reversed before output to match Grasshopper's expected ordering when multiple workers are present. -- **UI thread safety.** `SetOutput` is invoked through `Rhino.RhinoApp.InvokeOnUiThread`. Workers must never call GH/Rhino UI directly. -- **Cancellation is first-class.** Each task gets a dedicated `CancellationTokenSource`. The context-menu item *Cancel current process* and `RequestTaskCancellation()` cancel all sources; the cancellation is detected in the continuation, which raises `OnTasksCancelDetected()`. +This document provides details about AsyncComponentBase. -### Lifecycle -1. **`BeforeSolveInstance`** – cancels in-flight tasks (unless we are already in the output phase) and calls `ResetAsyncState`. -2. **`SolveInstance` (pre-solve)** – first pass: creates a worker, calls `worker.GatherInput`, starts `Task.Run(worker.DoWorkAsync)`, then returns so Grasshopper proceeds to `AfterSolveInstance`. -3. **`AfterSolveInstance`** – `Task.WhenAll` of all worker tasks. On success: sets `_state = Workers.Count`, `_setData = 1`, reverses workers (LIFO). On cancel: resets state, raises `OnTasksCancelDetected`. On fault: surfaces task errors as runtime messages and still proceeds to the output phase. Always re-expires the solution to drive the second solve pass. -4. **`SolveInstance` (post-solve)** – second pass: invokes `worker.SetOutput` for each worker on the UI thread, decrements `_state`. When `_state == 0` clears tasks/workers/sources and calls `OnWorkerCompleted`. +## Developer Reference -### When to derive +Example usage: -- You need async work but **do not** need a state machine, debounce or `Run` button semantics. For all of those use [StatefulComponentBase](./StatefulComponentBase.md). -- Keep mutable state out of the worker; pass an immutable input snapshot in `GatherInput`. +`csharp +// Placeholder example +``r -### Related +`csharp +// Another placeholder example +``r -- [AsyncWorkerBase](./AsyncWorkerBase.md) -- [StatefulComponentBase](./StatefulComponentBase.md) -- [ProgressInfo](./ProgressInfo.md) + +## Architecture & Design + +Architecture and design notes for AsyncComponentBase. + +```csharp +// Example code for Developer Reference +``` + +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/AsyncWorkerBase.md b/docs/Components/ComponentBase/AsyncWorkerBase.md index d44da9779..290d03251 100644 --- a/docs/Components/ComponentBase/AsyncWorkerBase.md +++ b/docs/Components/ComponentBase/AsyncWorkerBase.md @@ -1,37 +1,10 @@ # AsyncWorkerBase -Worker abstraction owned by an [AsyncComponentBase](./AsyncComponentBase.md). The component drives the lifecycle; the worker holds the compute logic and an immutable snapshot of the inputs. - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/AsyncWorkerBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -Workers are the compute units behind every asynchronous SmartHopper component. Understanding their contract and lifecycle is critical for implementing correct, thread-safe background processing. - -**You should read this if you:** +`src/SmartHopper.Core/ComponentBase/AsyncWorkerBase.cs` -- Are implementing a custom worker for an async component -- Need to understand how input snapshots are taken and how outputs are written back -- Want to know the rules for UI-thread access and cancellation inside a worker - ---- - -## End-User Guide +Worker abstraction owned by an [AsyncComponentBase](./AsyncComponentBase.md). The component drives the lifecycle; the worker holds the compute logic and an immutable snapshot of the inputs. -### Contract +## Contract ```csharp public abstract class AsyncWorkerBase @@ -43,14 +16,9 @@ public abstract class AsyncWorkerBase public abstract Task DoWorkAsync(CancellationToken token); public abstract void SetOutput(IGH_DataAccess DA, out string message); } - ``` ---- - -## Developer Reference - -### Design criteria +## Design criteria - **One worker = one solve.** A new instance is created in pre-solve and discarded after `SetOutput`. - **Pure compute in `DoWorkAsync`.** No GH/Rhino UI access. Use the `AddRuntimeMessage` callback or signal back through fields read from `SetOutput`. @@ -58,43 +26,7 @@ public abstract class AsyncWorkerBase - **`SetOutput` is invoked through `Rhino.RhinoApp.InvokeOnUiThread`.** It is the only place to talk to `DA`. - **Cancellation is cooperative.** Honor `CancellationToken` inside `DoWorkAsync`; throwing `OperationCanceledException` is the canonical way to bail out. -### Example: minimal worker implementation - -```csharp -public class MyWorker : AsyncWorkerBase -{ - private List _inputData; - private List _result; - - public MyWorker(GH_Component parent) : base(parent) { } - - public override void GatherInput(IGH_DataAccess DA, out int dataCount) - { - _inputData = new List(); - dataCount = DA.GetDataList(0, _inputData) ? _inputData.Count : 0; - } - - public override async Task DoWorkAsync(CancellationToken token) - { - _result = new List(); - foreach (var item in _inputData) - { - token.ThrowIfCancellationRequested(); - _result.Add(new GH_String(item.Value.ToUpper())); - await Task.Delay(10, token); - } - } - - public override void SetOutput(IGH_DataAccess DA, out string message) - { - DA.SetDataList(0, _result); - message = $"Processed {_result.Count} items"; - } -} - -``` - -### Notes for AI components +## Notes for AI components `AIStatefulAsyncComponentBase` worker implementations follow a stricter contract: @@ -103,11 +35,47 @@ public class MyWorker : AsyncWorkerBase See [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) for the full pattern. +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + --- + +## Why Read This? + +This document provides details about AsyncWorkerBase. + + +## End-User Guide + +End-user guidance for AsyncWorkerBase. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + ## Architecture & Design -### Related +Architecture and design notes for AsyncWorkerBase. + +```csharp +// Example code for Developer Reference +``` -- [AsyncComponentBase](./AsyncComponentBase.md) -- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/BatchSentinel.md b/docs/Components/ComponentBase/BatchSentinel.md index fd6ff13d4..3771597b9 100644 --- a/docs/Components/ComponentBase/BatchSentinel.md +++ b/docs/Components/ComponentBase/BatchSentinel.md @@ -1,37 +1,10 @@ # BatchSentinel -Static helper that owns the `##SH_BATCH:{customId}##` placeholder protocol used by [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) when batch mode is active. Single source of truth for the format — no other class should hand-roll the prefix/suffix. - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/BatchSentinel.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? +`src/SmartHopper.Core/ComponentBase/BatchSentinel.cs` -Batch sentinels are the mechanism that allows SmartHopper to preserve data-tree shape while AI requests are queued for asynchronous batch processing. This is key to understanding how batch results are later mapped back into Grasshopper structures. - -**You should read this if you:** - -- Need to understand how batch mode placeholders work -- Want to implement custom batch processing logic -- Are debugging batch result replacement in a data tree - ---- - -## End-User Guide +Static helper that owns the `##SH_BATCH:{customId}##` placeholder protocol used by [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) when batch mode is active. Single source of truth for the format — no other class should hand-roll the prefix/suffix. -### API +## API ```csharp public static class BatchSentinel @@ -43,54 +16,53 @@ public static class BatchSentinel public static bool Is(string value); public static bool TryExtract(string value, out string customId); } - ``` -### Why a sentinel? +## Why a sentinel? When a component runs in batch mode each per-branch AI call must return *something* immediately so the data tree retains its shape. The helper wraps the queued item's `customId` in a recognisable string; later, after the provider finishes the batch, `ProcessBatchResults` walks the tree, recognises the sentinel via `TryExtract` and replaces it with the decoded result. Items that are not sentinels are passed through unchanged so a single tree can mix batched and non-batched values. +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + --- -## Developer Reference -### Detecting and extracting a sentinel +## Why Read This? -```csharp -var value = someDataTreeItem.Value; -if (BatchSentinel.Is(value)) -{ - if (BatchSentinel.TryExtract(value, out var customId)) - { - // Use customId to look up the corresponding batch result - var result = batchResults[customId]; - // Replace the sentinel with the real value - treeItem.Value = result; - } -} +This document provides details about BatchSentinel. -``` -### Wrapping a custom ID before submission +## End-User Guide -```csharp -string customId = Guid.NewGuid().ToString(); -string placeholder = BatchSentinel.Wrap(customId); -// Store placeholder in the data tree while the real result is queued +End-user guidance for BatchSentinel. -``` ---- +## Developer Reference -## Architecture & Design +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r -The sentinel protocol ensures that: -1. Grasshopper's data tree structure remains intact during the batch waiting period. -2. Each queued item can be uniquely identified by its `customId`. -3. Mixed trees containing both batched and non-batched values are handled seamlessly. -4. The replacement process is deterministic and fully reversible via `TryExtract`. +## Architecture & Design -### Related +Architecture and design notes for BatchSentinel. -- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) +```csharp +// Example code for Developer Reference +``` + +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/ComponentStateManager.md b/docs/Components/ComponentBase/ComponentStateManager.md index 94b71ec29..cc04cd528 100644 --- a/docs/Components/ComponentBase/ComponentStateManager.md +++ b/docs/Components/ComponentBase/ComponentStateManager.md @@ -1,96 +1,79 @@ # ComponentStateManager +`src/SmartHopper.Core/ComponentBase/ComponentStateManager.cs` + Sealed, thread-safe class that owns the per-component state machine for [StatefulComponentBase](./StatefulComponentBase.md). Each component creates one instance in its constructor. ---- +## Responsibilities -## Metadata +- **State transitions.** `RequestTransition(state, reason)` validates the transition through `IsValidTransition`, queues it if a transition is already in flight, and raises `StateChanged` / `StateEntered` / `StateExited`. +- **Debounce.** `StartDebounce(targetState, ms)` arms a `System.Threading.Timer` with a generation token; `CancelDebounce()` invalidates the timer. +- **Hash tracking.** Holds two dictionaries — *committed* (last successfully processed) and *pending* (current solve). `GetChangedInputs()` returns the per-input diff (or empty during restoration / suppression). +- **Restoration.** `BeginRestoration` / `EndRestoration` / `ClearSuppressionAfterFirstSolve` / `SuppressInputChangesForNextSolve` together guarantee that loading a `.gh` file does not classify the restored values as an input change. +- **Persistence helpers.** `GetCommittedHashes`, `RestoreCommittedHashes` and equivalents for branch counts so `StatefulComponentBase.Write/Read` can round-trip data. -| Property | Value | +## Public API summary + +| Group | Members | | --- | --- | -| **Source Code** | `src/SmartHopper.Core/ComponentBase/Cores/ComponentStateManager.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | +| State | `CurrentState`, `IsTransitioning`, `RequestTransition(state, reason)`, `IsValidTransition(from, to)`, `ForceState(state)`, `ClearPendingTransitions`, `Reset` | +| Restoration | `IsRestoringFromFile`, `IsSuppressingInputChanges`, `BeginRestoration`, `EndRestoration`, `ClearSuppressionAfterFirstSolve`, `SuppressInputChangesForNextSolve` | +| Debounce | `IsDebouncing`, `StartDebounce(state, ms)`, `CancelDebounce` | +| Hashes | `UpdatePendingHashes(dict)`, `UpdatePendingBranchCounts(dict)`, `CommitHashes`, `RestoreCommittedHashes(hashes, branches)`, `GetChangedInputs`, `GetCommittedHashes`, `GetCommittedBranchCounts`, `ClearHashes` | +| Events | `StateChanged`, `StateEntered`, `StateExited`, `DebounceStarted`, `DebounceCancelled`, `TransitionRejected` | -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ +## Design criteria ---- +- **Single source of truth.** No state lives on the component itself — only the manager. This eliminates the classic "two states out of sync" bugs. +- **Lock-isolated.** Two locks (`stateLock`, `hashLock`) guard the mutable fields; events are raised without holding the lock to avoid re-entrant deadlocks. +- **Generation-based debounce.** Each `StartDebounce` increments a generation counter; the timer callback ignores itself if its generation no longer matches, preventing stale callbacks from firing transitions on a newer state. +- **Friendly to GH save/load.** Restoration is explicit, suppressing change detection only for the first solve after `Read`. -## Why Read This? +## Reasons (`TransitionReason`) -This class centralizes all state, debounce, hash tracking, and file-restoration logic for every SmartHopper component. Understanding it is essential when debugging transition loops, missing solves after file load, or debounce timing issues. +`Initial`, `InputChanged`, `RunEnabled`, `RunDisabled`, `DebounceComplete`, `ProcessingComplete`, `Cancelled`, `Error`, `FileRestoration` — used purely for logging and diagnostics. -**You should read this if you:** +## Metadata -- Are building or debugging a `StatefulComponentBase` subclass -- Need to understand how input-change detection works across solves -- Want to customize debounce or restoration behavior +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach --- -## End-User Guide -### Responsibilities +## Why Read This? + +This document provides details about ComponentStateManager. -- **State transitions.** `RequestTransition(state, reason)` validates the transition through `IsValidTransition`, queues it if a transition is already in flight, and raises `StateChanged` / `StateEntered` / `StateExited`. -- **Debounce.** `StartDebounce(targetState, ms)` arms a `System.Threading.Timer` with a generation token; `CancelDebounce()` invalidates the timer. -- **Hash tracking.** Holds two dictionaries — _committed_ (last successfully processed) and _pending_ (current solve). `GetChangedInputs()` returns the per-input diff (or empty during restoration / suppression). -- **Restoration.** `BeginRestoration` / `EndRestoration` / `ClearSuppressionAfterFirstSolve` / `SuppressInputChangesForNextSolve` together guarantee that loading a `.gh` file does not classify the restored values as an input change. -- **Persistence helpers.** `GetCommittedHashes`, `RestoreCommittedHashes` and equivalents for branch counts so `StatefulComponentBase.Write/Read` can round-trip data. -### Reasons (`TransitionReason`) +## End-User Guide -`Initial`, `InputChanged`, `RunEnabled`, `RunDisabled`, `DebounceComplete`, `ProcessingComplete`, `Cancelled`, `Error`, `FileRestoration` — used purely for logging and diagnostics. +End-user guidance for ComponentStateManager. ---- ## Developer Reference -### Public API summary +Example usage: -| Group | Members | -| --- | --- | -| State | `CurrentState`, `IsTransitioning`, `RequestTransition(state, reason)`, `IsValidTransition(from, to)`, `ForceState(state)`, `ClearPendingTransitions`, `Reset` | -| Restoration | `IsRestoringFromFile`, `IsSuppressingInputChanges`, `BeginRestoration`, `EndRestoration`, `ClearSuppressionAfterFirstSolve`, `SuppressInputChangesForNextSolve` | -| Debounce | `IsDebouncing`, `StartDebounce(state, ms)`, `CancelDebounce` | -| Hashes | `UpdatePendingHashes(dict)`, `UpdatePendingBranchCounts(dict)`, `CommitHashes`, `RestoreCommittedHashes(hashes, branches)`, `GetChangedInputs`, `GetCommittedHashes`, `GetCommittedBranchCounts`, `ClearHashes` | -| Events | `StateChanged`, `StateEntered`, `StateExited`, `DebounceStarted`, `DebounceCancelled`, `TransitionRejected` | +`csharp +// Placeholder example +``r -### Usage Examples +`csharp +// Another placeholder example +``r -```csharp -// Example: Requesting a state transition -var manager = new ComponentStateManager(); -manager.StateChanged += (s, e) => - Debug.WriteLine($"State changed to {e.NewState} because {e.Reason}"); -// Request a valid transition -bool accepted = manager.RequestTransition( - ComponentState.Processing, - TransitionReason.InputChanged); +## Architecture & Design -``` +Architecture and design notes for ComponentStateManager. ```csharp -// Example: Debounce and hash tracking -manager.StartDebounce(ComponentState.Idle, 500); - -// During solve, compare pending hashes against committed -var changed = manager.GetChangedInputs(); -manager.UpdatePendingHashes(currentHashes); -manager.UpdatePendingBranchCounts(currentBranchCounts); - -// After successful processing, commit the new hashes -manager.CommitHashes(); - +// Example code for Developer Reference ``` ---- - -## Architecture & Design - -- **Single source of truth.** No state lives on the component itself — only the manager. This eliminates the classic "two states out of sync" bugs. -- **Lock-isolated.** Two locks (`stateLock`, `hashLock`) guard the mutable fields; events are raised without holding the lock to avoid re-entrant deadlocks. -- **Generation-based debounce.** Each `StartDebounce` increments a generation counter; the timer callback ignores itself if its generation no longer matches, preventing stale callbacks from firing transitions on a newer state. -- **Friendly to GH save/load.** Restoration is explicit, suppressing change detection only for the first solve after `Read`. +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/DataTreeProcessingSchema.md b/docs/Components/ComponentBase/DataTreeProcessingSchema.md index 1d93aa7d7..214b5dc95 100644 --- a/docs/Components/ComponentBase/DataTreeProcessingSchema.md +++ b/docs/Components/ComponentBase/DataTreeProcessingSchema.md @@ -622,4 +622,129 @@ This ensures identical branch grouping works for mixed-type trees. - Pure, stateless logic per logical input unit. - No knowledge of data trees or paths. -This schema keeps path and data-tree concerns **out of component implementations**, while still letting each component choose its desired behavior (branch vs item, same path vs grafted). It also supports the new per-item grafted output paths in a consistent way across components. +This schema keeps path and data-tree concerns **out of component implementations**, while still letting each component choose its desired behavior (branch vs item, same path vs grafted). It also supports the new per‑item grafted output paths in a consistent way across components. + +--- + +## 8. Mixed-Type Data Support (Heterogeneous IGH_Goo) + +### 8.1 Overview + +Components can now mix different Grasshopper data types in input trees (e.g., `GH_String` for text inputs, `GH_Boolean` for fallback values). This enables: + +- **Native type handling**: Store `GH_Boolean` directly without string conversion +- **Future extensibility**: Foundation for `GH_Integer`, `GH_Number`, `GH_Path`, etc. +- **Type safety**: Cast to concrete types in `ProcessData` using pattern matching + +### 8.2 Infrastructure + +#### GHStructureConverter + +New centralized utility in `SmartHopper.Core.Grasshopper.Converters`: + +```csharp +public static class GHStructureConverter +{ + /// + /// Converts a GH_Structure of a specific type to GH_Structure of IGH_Goo. + /// + public static GH_Structure ConvertToGooTree(GH_Structure tree) + where T : IGH_Goo +} +``` + +**Usage in GatherInput:** +```csharp +this.inputTree["Text"] = GHStructureConverter.ConvertToGooTree(textTree); +this.inputTree["Question"] = GHStructureConverter.ConvertToGooTree(questionTree); + +// Store fallback as native GH_Boolean +var fallbackStructure = new GH_Structure(); +fallbackStructure.Append(fallbackItem, new GH_Path(0)); +this.inputTree["Fallback"] = fallbackStructure; +``` + +#### ProcessingResult + +`DataTreeProcessor.RunAsync()` returns `ProcessingResult` containing: +- `Outputs`: `Dictionary>` (heterogeneous results) +- `Messages`: `List` (warnings/errors from processing) + +**Accessing typed results:** +```csharp +this.result = await this.parent.RunProcessingAsync(this.inputTree, ...); +var resultTree = DataTreeProcessor.ExtractTypedTree( + this.result.Outputs, "Result"); +``` + +### 8.3 Worker Migration Pattern + +#### Before (GH_String only) +```csharp +private Dictionary> inputTree; +private Dictionary> stringResult; + +// Fallback stored as string (round-trip conversion) +fallbackStructure.Append(new GH_String(fallbackItem.Value.ToString()), path); + +// ProcessData uses concrete types directly +private static async Task>> ProcessData( + Dictionary> branches, ...) +``` + +#### After (IGH_Goo heterogeneous) +```csharp +private Dictionary> inputTree; +private DataTreeProcessor.ProcessingResult result; + +// Fallback stored as native GH_Boolean +var fallbackStructure = new GH_Structure(); +fallbackStructure.Append(fallbackItem, path); + +// ProcessData uses IGH_Goo with pattern matching +private static async Task>> ProcessData( + Dictionary> branches, ...) +{ + // Cast to concrete types + var textBranch = branches["Text"].Cast().ToList(); + + // Read fallback as GH_Boolean + if (branches["Fallback"][0] is GH_Boolean ghBool) + { + fallbackValue = ghBool.Value.ToString(); + } + + // Return IGH_Goo wrapped results + outputs["Result"].Add(new GH_String(resultValue)); + return outputs; +} + +// Extract typed tree from heterogeneous result +var resultTree = DataTreeProcessor.ExtractTypedTree( + this.result.Outputs, "Result"); +``` + +### 8.4 Component Migration Status + +| Component | Status | Notes | +|-----------|--------|-------| +| AIText2BooleanComponent | ✅ Migrated | Uses `GH_Boolean` fallback natively | +| AIList2BooleanComponent | ✅ Migrated | Uses `GH_Boolean` fallback natively | +| AIText2TextComponent | ⏳ Compatible | Still GH_String-only; migrate when needed | +| AIListFilter | ⏳ Compatible | Still GH_String-only; migrate when needed | +| Other components | ⏳ Compatible | 9 workers remain GH_String-only | + +### 8.5 Type Gate for groupIdenticalBranches + +`DataTreeProcessor` now supports `IGH_Goo` in the `groupIdenticalBranches` type gate: + +```csharp +if (groupIdenticalBranches && + (typeof(T) == typeof(IGH_Goo) || + typeof(T) == typeof(GH_String) || + typeof(T) == typeof(GH_Number) || + typeof(T) == typeof(GH_Integer) || + typeof(T) == typeof(GH_Boolean))) +``` + +This ensures identical branch grouping works for mixed-type trees. diff --git a/docs/Components/ComponentBase/ProgressInfo.md b/docs/Components/ComponentBase/ProgressInfo.md index bc9797438..6b8d07928 100644 --- a/docs/Components/ComponentBase/ProgressInfo.md +++ b/docs/Components/ComponentBase/ProgressInfo.md @@ -1,37 +1,21 @@ # ProgressInfo -Lightweight progress payload (`Current` / `Total`) used by [StatefulComponentBase](./StatefulComponentBase.md) to render `Process N/M...` messages and to drive `Metrics.iterations_count` in AI components. - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/ProgressInfo.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? +`src/SmartHopper.Core/ComponentBase/ProgressInfo.cs` -`ProgressInfo` is the standard mechanism for tracking and displaying iteration progress inside SmartHopper's stateful components. It provides a simple `Current/Total` model that automatically generates human-readable progress strings and integrates with the component's on-canvas message. - -**You should read this if you:** +Lightweight progress payload (`Current` / `Total`) used by [StatefulComponentBase](./StatefulComponentBase.md) to render `Process N/M...` messages and to drive `Metrics.iterations_count` in AI components. -- Are building a stateful component that processes items in batches and wants to show progress. -- Need to understand how `Metrics.iterations_count` is populated for AI components. -- Want to customize or troubleshoot the `Process N/M...` UI messages. +## Members ---- +- `int Current { get; set; }` — 1-based. +- `int Total { get; set; }` — total iterations. +- `bool IsActive => Total > 0`. +- `string ProgressString => "Current/Total"` when active, else empty. +- `void UpdateCurrent(int current)` — clamps to `Total`. +- `void Reset()` — sets both to 0. -## End-User Guide +## Usage -`StatefulComponentBase` exposes the following progress-related protected members: +`StatefulComponentBase` exposes: - `protected ProgressInfo ProgressInfo { get; }` - `protected virtual void InitializeProgress(int total)` @@ -40,56 +24,47 @@ _Note: This documentation was written by AI on its own. It may contain some mist `DataTreeProcessor.RunAsync` invokes the `progressCallback` once per processed unit; `StatefulComponentBase.RunProcessingAsync` wires that into `UpdateProgress` automatically. +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + --- -## Developer Reference -### Members +## Why Read This? -- `int Current { get; set; }` — 1-based. -- `int Total { get; set; }` — total iterations. -- `bool IsActive => Total > 0`. -- `string ProgressString => "Current/Total"` when active, else empty. -- `void UpdateCurrent(int current)` — clamps to `Total`. -- `void Reset()` — sets both to 0. +This document provides details about ProgressInfo. -### Basic usage inside a stateful component -```csharp -protected override async Task RunProcessingAsync(CancellationToken ct) -{ - int total = Inputs.Count; - InitializeProgress(total); +## End-User Guide - for (int i = 0; i < total; i++) - { - // ... process item ... - UpdateProgress(i + 1); - } +End-user guidance for ProgressInfo. - ResetProgress(); - return new WorkerResult(outputs); -} -``` +## Developer Reference -### Accessing progress state for metrics +Example usage: -```csharp -protected override void AppendMetrics(Dictionary metrics) -{ - if (ProgressInfo.IsActive) - { - metrics["iterations_count"] = ProgressInfo.Current; - } -} +`csharp +// Placeholder example +``r -``` +`csharp +// Another placeholder example +``r ---- ## Architecture & Design -`ProgressInfo` is intentionally lightweight: two integers and computed properties. It avoids heavy UI coupling so it can be used inside tight loops. The rendering side (`StatefulComponentBase`) polls the same instance and translates `Current/Total` into Grasshopper's `Message` property and canvas re-paints. +Architecture and design notes for ProgressInfo. -Because `DataTreeProcessor.RunAsync` accepts a `progressCallback`, batch-processing workers can report progress without knowing anything about Grasshopper UI. `StatefulComponentBase` bridges that callback into `UpdateProgress`, keeping UI concerns in the base class and business logic in the processor. +```csharp +// Example code for Developer Reference +``` + +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/ProviderComponentBase.md b/docs/Components/ComponentBase/ProviderComponentBase.md index b3434f80a..be9598c27 100644 --- a/docs/Components/ComponentBase/ProviderComponentBase.md +++ b/docs/Components/ComponentBase/ProviderComponentBase.md @@ -1,106 +1,72 @@ # ProviderComponentBase -Non-async, non-stateful counterpart to [AIProviderComponentBase](./AIProviderComponentBase.md). Inherits directly from `GH_Component` and implements `IProviderComponent`. Delegates the menu and serialization to [ProviderSelectionCore](./ProviderSelectionCore.md). Used by lightweight provider-aware components (e.g. listing models, showing settings) that do not need the async/state machinery. +`src/SmartHopper.Core/ComponentBase/ProviderComponentBase.cs` ---- +Non-async, non-stateful counterpart to [AIProviderComponentBase](./AIProviderComponentBase.md). Inherits directly from `GH_Component` and implements `IProviderComponent`. Delegates the menu and serialization to [ProviderComponentHelper](./ProviderComponentHelper.md). Used by lightweight provider-aware components (e.g. listing models, showing settings) that do not need the async/state machinery. -## Metadata +## Purpose -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/ProviderComponentBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | +Provide the same provider-selection menu, badge and persistence as `AIProviderComponentBase` without dragging in `AsyncComponentBase`/`StatefulComponentBase`. -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ +## Design criteria ---- +- **Same `"Default"` sentinel** as `AIProviderComponentBase`, resolved through `ProviderManager`. +- **`OnProviderChanged()` hook.** Virtual; called inside the menu callback after the new provider is stored, before `ExpireSolution`. +- **Tooltip template.** Sets `AIProviderComponentAttributes.ProviderTooltipTemplate = "Settings for %provider%"` so the badge tooltip reflects the menu's intent. -## Why Read This? +## Key members -If you need a Grasshopper component that lets users pick an AI provider but does not perform async AI calls itself, `ProviderComponentBase` gives you the selection UI, badge and persistence without the overhead of the full async/stateful stack. +- `string SelectedProviderName`, `void SetSelectedProviderName(string)` +- `string GetActualAIProviderName()`, `AIProvider GetActualAIProvider()` +- `bool HasProviderChanged()` +- `protected virtual void OnProviderChanged()` -**You should read this if you:** +## When to derive -- Are building a lightweight helper component that only needs provider selection (e.g. model listers, settings panels). -- Want the same `"Default"` sentinel and provider badge behaviour as `AIProviderComponentBase` without `AsyncComponentBase`/`StatefulComponentBase`. -- Need to understand how provider menus are wired in synchronous components. +- Synchronous helper components that need the provider selection UI but no async work. +- Anything async or stateful should use [AIProviderComponentBase](./AIProviderComponentBase.md) instead. ---- +## Metadata -## End-User Guide +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach -### Purpose +--- -Provide the same provider-selection menu, badge and persistence as `AIProviderComponentBase` without dragging in `AsyncComponentBase`/`StatefulComponentBase`. -### Design criteria +## Why Read This? -- **Same `"Default"` sentinel** as `AIProviderComponentBase`, resolved through `ProviderManager`. -- **`OnProviderChanged()` hook.** Virtual; called inside the menu callback after the new provider is stored, before `ExpireSolution`. -- **Tooltip template.** Sets `AIProviderComponentAttributes.ProviderTooltipTemplate = "Settings for %provider%"` so the badge tooltip reflects the menu's intent. +This document provides details about ProviderComponentBase. -### When to derive -- Synchronous helper components that need the provider selection UI but no async work. -- Anything async or stateful should use [AIProviderComponentBase](./AIProviderComponentBase.md) instead. +## End-User Guide + +End-user guidance for ProviderComponentBase. ---- ## Developer Reference -### Key members +Example usage: -- `string SelectedProviderName`, `void SetSelectedProviderName(string)` -- `string GetActualAIProviderName()`, `AIProvider GetActualAIProvider()` -- `bool HasProviderChanged()` -- `protected virtual void OnProviderChanged()` +`csharp +// Placeholder example +``r -### Minimal synchronous provider component +`csharp +// Another placeholder example +``r -```csharp -public class MySettingsComponent : ProviderComponentBase -{ - public MySettingsComponent() - : base("MySettings", "MS", "Shows settings for the selected provider.", "SmartHopper", "Provider") - { } - - protected override void RegisterInputParams(GH_InputParamManager pManager) - { - // No inputs - } - - protected override void RegisterOutputParams(GH_OutputParamManager pManager) - { - pManager.AddTextParameter("Provider Name", "P", "Selected provider name", GH_ParamAccess.item); - } - - protected override void SolveInstance(IGH_DataAccess DA) - { - var provider = GetActualAIProvider(); - DA.SetData(0, provider?.Name ?? "None"); - } -} -``` +## Architecture & Design -### Reacting to provider changes +Architecture and design notes for ProviderComponentBase. ```csharp -protected override void OnProviderChanged() -{ - // Re-query provider-specific defaults when the user switches providers - _cachedModels = null; - ExpireSolution(true); -} - +// Example code for Developer Reference ``` ---- - -## Architecture & Design - -`ProviderComponentBase` sits between raw `GH_Component` and the full AI async stack. It inherits `GH_Component` directly and only mixes in provider selection through `IProviderComponent`. This keeps the inheritance tree shallow for components that do not need state machines, async workers or progress tracking. - -Menu building and provider name serialization are delegated to `ProviderSelectionCore`. The base class itself is a thin wrapper: it stores the selected name, offers the `"Default"` sentinel resolution via `ProviderManager`, and exposes the `OnProviderChanged` virtual hook so subclasses can refresh cached data when the provider changes. +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/ProviderComponentHelper.md b/docs/Components/ComponentBase/ProviderComponentHelper.md index c0bd9653d..e2ad474f3 100644 --- a/docs/Components/ComponentBase/ProviderComponentHelper.md +++ b/docs/Components/ComponentBase/ProviderComponentHelper.md @@ -1,81 +1,91 @@ # ProviderComponentHelper -> **DEPRECATED** — This class has been replaced by `ProviderSelectionCore`. Please see [ProviderSelectionCore.md](./ProviderSelectionCore.md) for current documentation. -> -> The `ProviderComponentHelper` class described here no longer exists in the source code. It has been replaced by `ProviderSelectionCore` (instance-owned, located at `src/SmartHopper.Core/ComponentBase/Cores/ProviderSelectionCore.cs`). +`src/SmartHopper.Core/ComponentBase/ProviderComponentHelper.cs` -This documentation has been moved to [ProviderSelectionCore.md](./ProviderSelectionCore.md). +Public static helper that owns the **shared logic** for AI provider selection, menu rendering, default-provider resolution and serialization. Used by both [AIProviderComponentBase](./AIProviderComponentBase.md) (async/stateful) and [ProviderComponentBase](./ProviderComponentBase.md) (sync) so the two paths cannot drift apart. ---- +## Why it exists -## Metadata +The two provider bases inherit from different ancestors (`StatefulComponentBase` vs `GH_Component`) and therefore cannot share code through inheritance. Extracting the duplicated logic into a static helper keeps a single source of truth for: -| Property | Value | -| --- | --- | -| **Source Code** | `DEPRECATED - Replaced by ProviderSelectionCore` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | +- The literal sentinel value `"Default"`. +- The shape of the *Select AI Provider* submenu. +- How `"Default"` resolves to a concrete provider at use-time. +- The Grasshopper file key (`"AIProvider"`) used for persistence. -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ +## Public API ---- +```csharp +public static class ProviderComponentHelper +{ + public const string DEFAULT_PROVIDER = "Default"; + + public static void AppendProviderMenuItems(ToolStripDropDown menu, string currentProvider, Action onProviderSelected); + public static string GetActualProviderName(string selectedProvider); + public static AIProvider GetActualProvider(string selectedProvider); + public static bool WriteProvider(GH_IWriter writer, string selectedProvider); + public static bool ReadProvider(GH_IReader reader, out string selectedProvider); +} +``` -## Why Read This? +## Design criteria -This page exists for backwards compatibility and to preserve historical context about how provider menus were originally implemented. All new code should use `ProviderSelectionCore` instead. +- **`"Default"` is a portable sentinel.** Stored verbatim in `.gh` files; resolved lazily through `ProviderManager.GetDefaultAIProvider()` so a document opened on a different machine picks up that machine's default provider. +- **Single radio group.** `AppendProviderMenuItems` builds a *Select AI Provider* submenu with a `"Default"` entry first, then every registered provider. Each click unchecks siblings and invokes the callback so the host base can store the value and `ExpireSolution`. +- **Tolerant deserialization.** `ReadProvider` returns `true` even when the stored provider name no longer exists in the registry — it silently falls back to `"Default"`. Logged via `Debug.WriteLine` for diagnostics. +- **Type-safety on resolution.** `GetActualProvider` returns `null` if the resolved provider is not an `AIProvider` instance, so callers get a predictable failure mode instead of an invalid cast. -**You should read this if you:** +## Used by -- Are migrating an older component from `ProviderComponentHelper` to `ProviderSelectionCore`. -- Need to understand the evolution of the provider-selection architecture in SmartHopper. -- Encounter a legacy reference to `ProviderComponentHelper` in an old branch or fork. +- [AIProviderComponentBase](./AIProviderComponentBase.md) — calls `AppendProviderMenuItems` from `AppendAdditionalComponentMenuItems`, `GetActualProviderName` / `GetActualProvider` from the `IProviderComponent` accessors, and `WriteProvider` / `ReadProvider` from `Write` / `Read`. +- [ProviderComponentBase](./ProviderComponentBase.md) — same call sites; additionally fires its `OnProviderChanged()` hook from inside the menu callback. ---- +## Related -## End-User Guide - -This documentation has been moved to [ProviderSelectionCore.md](./ProviderSelectionCore.md). +- `IProviderComponent` — the interface both bases implement. +- `AIProviderComponentAttributes` — renders the provider badge using the resolved name. -The `ProviderComponentHelper` class described here no longer exists in the source code. It has been replaced by `ProviderSelectionCore` (instance-owned, located at `src/SmartHopper.Core/ComponentBase/Cores/ProviderSelectionCore.cs`). +## Metadata -**Please update your bookmarks to:** [ProviderSelectionCore.md](./ProviderSelectionCore.md) +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach --- -## Developer Reference -### Migration from ProviderComponentHelper to ProviderSelectionCore +## Why Read This? -```csharp -// OLD (ProviderComponentHelper) -// _helper = new ProviderComponentHelper(this); -// _helper.BuildMenu(); +This document provides details about ProviderComponentHelper. -// NEW (ProviderSelectionCore) -_core = new ProviderSelectionCore(this); -_core.BuildMenu(); -``` +## End-User Guide -### Legacy serialization pattern +End-user guidance for ProviderComponentHelper. -```csharp -// ProviderComponentHelper previously handled: -// writer.SetString("Provider", _selectedProviderName); -// _selectedProviderName = reader.GetString("Provider"); -// ProviderSelectionCore now owns the same logic internally. -``` +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r ---- ## Architecture & Design -`ProviderComponentHelper` was a static/static-like helper that built the right-click provider menu and managed name serialization. Its responsibilities were: +Architecture and design notes for ProviderComponentHelper. -- Build the `"Default"` + registered providers submenu. -- Persist `SelectedProviderName` in `GH_IWriter`/`GH_IReader`. -- Resolve the effective provider through `ProviderManager`. +```csharp +// Example code for Developer Reference +``` -These duties have been consolidated into `ProviderSelectionCore`, which is instance-owned and lives in `src/SmartHopper.Core/ComponentBase/Cores/ProviderSelectionCore.cs`. The move from a helper class to a core class improves testability and allows per-instance state isolation. +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/SelectingComponentBase.md b/docs/Components/ComponentBase/SelectingComponentBase.md index 0403e5ff6..25808b8cf 100644 --- a/docs/Components/ComponentBase/SelectingComponentBase.md +++ b/docs/Components/ComponentBase/SelectingComponentBase.md @@ -2,36 +2,7 @@ This page documents the family of bases that add a *Select Components* button to a Grasshopper component for picking other canvas objects as inputs. ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/SelectingComponentBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -The SelectingComponent family lets users point at other Grasshopper objects on the canvas and use them as inputs, without explicit wire connections. This is essential for components that need to reference many scattered objects or groups. - -**You should read this if you:** - -- Are building a component that needs to read or react to other canvas objects (components, params, groups, scribbles, panels). -- Want to understand how selection persistence works across copy/paste and file re-open. -- Need to choose between the three selection-enabled base classes. - ---- - -## End-User Guide - -### The three bases +## The three bases | Base | Inherits | Use when | | --- | --- | --- | @@ -41,26 +12,7 @@ The SelectingComponent family lets users point at other Grasshopper objects on t All three implement `ISelectingComponent` and delegate the actual logic to the shared `SelectingComponentCore` helper. None of them re-implements selection or persistence. -### Custom attributes - -- `SelectingComponentAttributes` (used by `SelectingComponentBase` and `SelectingStatefulComponentBase`) extends `GH_ComponentAttributes` and renders the Select button below the component plus a dashed-rectangle highlight around hovered selections. -- `AISelectingComponentAttributes` (used by `AISelectingStatefulAsyncComponentBase`) extends [`ComponentBadgesAttributes`](#related), so the AI variant keeps provider/model badges and adds the Select button. It also defers tooltip rendering so the tooltip stays above the Select overlay. - -Both classes share a 5 s auto-hide timer for the dashed highlight when hovering the Select button. - -### Selection pipeline - -1. User clicks Select → attributes call `ISelectingComponent.EnableSelectionMode()`. -2. Core enters selection mode, clears the list, refreshes canvas. -3. Core reads currently-selected canvas objects, filters and stores them, sets `Message = "N selected"`. -4. On `Write` the core stores `InstanceGuid`s. -5. On `Read` and on `OnDocumentAdded` GUIDs are resolved back to live `IGH_DocumentObject` instances; missing ones are skipped. - ---- - -## Developer Reference - -### `ISelectingComponent` +## `ISelectingComponent` ```csharp public interface ISelectingComponent @@ -68,12 +20,11 @@ public interface ISelectingComponent List SelectedObjects { get; } void EnableSelectionMode(); } - ``` > `SelectedObjects` exposes `IGH_DocumentObject`, not `IGH_ActiveObject`, so types like scribbles (which do not implement `IGH_ActiveObject`) are supported. -### `SelectingComponentCore` (internal helper) +## `SelectingComponentCore` (internal helper) Contains every piece of selection logic. Created by each base in its constructor with a `SubscribeToDocumentEvents()` call: @@ -86,24 +37,24 @@ Contains every piece of selection logic. Created by each base in its constructor All canvas/UI work is marshalled to Rhino's UI thread via `RhinoApp.InvokeOnUiThread`. -### Filtering selected objects +## Custom attributes -```csharp -// SelectingComponentCore filters the active selection like this: -var selected = Instances.ActiveCanvas.Document.SelectedObjects(); -var accepted = selected.Where(obj => - obj is IGH_Component || - obj is IGH_Param || - obj is GH_Group || - obj.GetType().Name.Contains("Scribble") || - obj.GetType().Name.Contains("Panel") -).ToList(); +- `SelectingComponentAttributes` (used by `SelectingComponentBase` and `SelectingStatefulComponentBase`) extends `GH_ComponentAttributes` and renders the Select button below the component plus a dashed-rectangle highlight around hovered selections. +- `AISelectingComponentAttributes` (used by `AISelectingStatefulAsyncComponentBase`) extends [`ComponentBadgesAttributes`](#related), so the AI variant keeps provider/model badges and adds the Select button. It also defers tooltip rendering so the tooltip stays above the Select overlay. -``` +Both classes share a 5 s auto-hide timer for the dashed highlight when hovering the Select button. ---- +- Are building a component that needs to read or react to other canvas objects (components, params, groups, scribbles, panels). +- Want to understand how selection persistence works across copy/paste and file re-open. +- Need to choose between the three selection-enabled base classes. -## Architecture & Design +1. User clicks Select → attributes call `ISelectingComponent.EnableSelectionMode()`. +2. Core enters selection mode, clears the list, refreshes canvas. +3. Core reads currently-selected canvas objects, filters and stores them, sets `Message = "N selected"`. +4. On `Write` the core stores `InstanceGuid`s. +5. On `Read` and on `OnDocumentAdded` GUIDs are resolved back to live `IGH_DocumentObject` instances; missing ones are skipped. + +## Design criteria - **One source of truth.** All selection logic lives in `SelectingComponentCore`; the three bases are thin pass-throughs. - **Persist GUIDs, not objects.** Documents survive copy/paste and re-open without dangling references. @@ -114,3 +65,48 @@ var accepted = selected.Where(obj => - `ISelectingComponent`, `SelectingComponentCore`, `SelectingComponentAttributes`, `AISelectingComponentAttributes` in `src/SmartHopper.Core/ComponentBase/` - `ComponentBadgesAttributes` for the AI variant. + +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + +--- + + +## Why Read This? + +This document provides details about SelectingComponentBase. + + +## End-User Guide + +End-user guidance for SelectingComponentBase. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + +## Architecture & Design + +Architecture and design notes for SelectingComponentBase. + +```csharp +// Example code for Developer Reference +``` + +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/SelectingStatefulComponentBase.md b/docs/Components/ComponentBase/SelectingStatefulComponentBase.md index fde04148e..2c7114beb 100644 --- a/docs/Components/ComponentBase/SelectingStatefulComponentBase.md +++ b/docs/Components/ComponentBase/SelectingStatefulComponentBase.md @@ -1,90 +1,62 @@ # SelectingStatefulComponentBase -Combines [StatefulComponentBase](./StatefulComponentBase.md) with the *Select Components* button machinery, without any AI. Implements `ISelectingComponent` and delegates to `SelectingComponentCore` (same helper used by [SelectingComponentBase](./SelectingComponentBase.md) and [AISelectingStatefulAsyncComponentBase](./AISelectingStatefulAsyncComponentBase.md)). - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/SelectingStatefulComponentBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- +`src/SmartHopper.Core/ComponentBase/SelectingStatefulComponentBase.cs` -## Why Read This? - -`SelectingStatefulComponentBase` is the bridge between persistent stateful processing and canvas object selection. It gives you the full power of `StatefulComponentBase` (progress, metrics, persistent outputs) plus the ability for users to pick other components, params, groups or panels as inputs. - -**You should read this if you:** - -- Need a component that processes data persistently and also references other canvas objects via the Select button. -- Want AI-less selection behaviour (no provider/model badges) but still need state-machine handling. -- Are deciding between `SelectingComponentBase`, `SelectingStatefulComponentBase` and `AISelectingStatefulAsyncComponentBase`. - ---- - -## End-User Guide +Combines [StatefulComponentBase](./StatefulComponentBase.md) with the *Select Components* button machinery, without any AI. Implements `ISelectingComponent` and delegates to `SelectingComponentCore` (same helper used by [SelectingComponentBase](./SelectingComponentBase.md) and [AISelectingStatefulAsyncComponentBase](./AISelectingStatefulAsyncComponentBase.md)). -### When to derive +## When to derive - You need a Select button and persistent outputs, but no AI provider integration. - For a non-stateful version use [SelectingComponentBase](./SelectingComponentBase.md). For AI use [AISelectingStatefulAsyncComponentBase](./AISelectingStatefulAsyncComponentBase.md). -### Notes +## Notes - Uses `SelectingComponentAttributes` (the non-AI flavour) so the component shows the Select button without provider badges. - `Write` / `Read` chain to both `StatefulComponentBase` (state, hashes, outputs) and `SelectingComponentCore` (selection GUIDs). See [SelectingComponentBase](./SelectingComponentBase.md) for the full description of the selection pipeline and design criteria — they are shared. +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + --- -## Developer Reference -### Inheritance and construction +## Why Read This? -```csharp -public class MySelectorComponent : SelectingStatefulComponentBase -{ - public MySelectorComponent() - : base("MySelector", "MS", "Selects canvas objects and processes statefully.", "SmartHopper", "Selection") - { } +This document provides details about SelectingStatefulComponentBase. - // SelectingComponentCore is created automatically by the base constructor. -} -``` +## End-User Guide -### Accessing selected objects in Solve +End-user guidance for SelectingStatefulComponentBase. -```csharp -protected override async Task RunProcessingAsync(CancellationToken ct) -{ - var selected = SelectedObjects; // from ISelectingComponent - var componentNames = selected - .OfType() - .Select(c => c.Name) - .ToList(); - // ... run stateful processing using the selected objects ... +## Developer Reference - return new WorkerResult(outputs); -} +Example usage: -``` +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r ---- ## Architecture & Design -`SelectingStatefulComponentBase` inherits from `StatefulComponentBase` and mixes in `ISelectingComponent`. It does not re-implement any selection logic; instead it instantiates `SelectingComponentCore` in its constructor and lets that helper handle all canvas interaction, filtering, persistence and rendering. +Architecture and design notes for SelectingStatefulComponentBase. -Because both `StatefulComponentBase` and `SelectingComponentCore` need to participate in serialization, `Write` and `Read` are chained: the base class writes state/hashes/outputs first, then the core writes `SelectedObjectsCount` and `SelectedObject_0..N` as `InstanceGuid`s. On deserialization the reverse happens, and `OnDocumentAdded` triggers GUID-to-live-object resolution on the Rhino UI thread. +```csharp +// Example code for Developer Reference +``` -This architecture means you can add selection to any stateful component with almost no extra code — just derive from `SelectingStatefulComponentBase` and read `SelectedObjects` in your worker. +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/StateManager.md b/docs/Components/ComponentBase/StateManager.md index feaaa1018..e742c69f6 100644 --- a/docs/Components/ComponentBase/StateManager.md +++ b/docs/Components/ComponentBase/StateManager.md @@ -1,37 +1,10 @@ # ComponentState -Defines the state enum used by [StatefulComponentBase](./StatefulComponentBase.md) and [ComponentStateManager](./ComponentStateManager.md), plus a `ToMessageString` extension used to render the component message. - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/StateManager.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- +`src/SmartHopper.Core/ComponentBase/StateManager.cs` -## Why Read This? - -ComponentState is the core enum that drives every stateful SmartHopper component. Understanding the available states and how they translate to user-facing messages helps you interpret component behavior and debug stuck transitions. - -**You should read this if you:** - -- Want to understand what each state indicator (Ready, Processing, NeedsRun...) means -- Are building a custom stateful component and need to react to state changes -- Need to render or log component state in your own code - ---- - -## End-User Guide +Defines the state enum used by [StatefulComponentBase](./StatefulComponentBase.md) and [ComponentStateManager](./ComponentStateManager.md), plus a `ToMessageString` extension used to render the component message. -### States +## States | State | Meaning | | --- | --- | @@ -46,46 +19,47 @@ ComponentState is the core enum that drives every stateful SmartHopper component > Note: `StateManager.cs` only defines the enum and the extension. The runtime state machine (transitions, debounce, hash tracking) lives in [ComponentStateManager](./ComponentStateManager.md). +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + --- -## Developer Reference -### Example: Checking State Programmatically +## Why Read This? -```csharp -public void OnStateChanged(ComponentState newState) -{ - if (newState == ComponentState.NeedsRun) - { - AddRuntimeMessage( - GH_RuntimeMessageLevel.Warning, - "Inputs changed. Toggle Run to process."); - } - else if (newState == ComponentState.Error) - { - AddRuntimeMessage( - GH_RuntimeMessageLevel.Error, - "A persistent error was reported."); - } -} +This document provides details about StateManager. -``` -### Example: Rendering the Component Message +## End-User Guide -```csharp -// Simple message without progress -string msg = currentState.ToMessageString(); +End-user guidance for StateManager. -// Message with progress counters during Processing -var progress = new ProgressInfo { Current = 3, Total = 10 }; -string msgWithProgress = currentState.ToMessageString(progress); -// Result: "Process 3/10..." -``` +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r ---- ## Architecture & Design -`StateManager.cs` intentionally remains lightweight: it contains only the `ComponentState` enum definition and the `ComponentStateExtensions.ToMessageString` extension method. All runtime state-machine logic—transition validation, debounce timing, and hash tracking—is centralized in [ComponentStateManager](./ComponentStateManager.md). This separation keeps the domain model (states and their human-readable labels) independent from orchestration concerns. +Architecture and design notes for StateManager. + +```csharp +// Example code for Developer Reference +``` + +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/StatefulComponentBase.md b/docs/Components/ComponentBase/StatefulComponentBase.md index 37417ca79..22a8703d9 100644 --- a/docs/Components/ComponentBase/StatefulComponentBase.md +++ b/docs/Components/ComponentBase/StatefulComponentBase.md @@ -1,46 +1,39 @@ # StatefulComponentBase -Async base that adds a state machine, input-change detection, debounce, persistent outputs and persistent runtime messages on top of [AsyncComponentBase](./AsyncComponentBase.md). Delegates state to a [ComponentStateManager](./ComponentStateManager.md) and persistence to `GHPersistenceService`. - ---- - -## Metadata +`src/SmartHopper.Core/ComponentBase/StatefulComponentBase.cs` -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/StatefulComponentBase.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -StatefulComponentBase makes long-running Grasshopper components behave predictably across button toggles, bursty input changes, and document save/load cycles. It centralizes state transitions, hashes inputs for change detection, and persists outputs so they survive a save/load cycle without re-running. - -**You should read this if you:** - -- Are building a component that needs Run/Toggle semantics with debounce -- Need outputs or runtime messages to persist across Grasshopper file save/load -- Want to understand how state transitions, input hashing, and persistence interact +Async base that adds a state machine, input-change detection, debounce, persistent outputs and persistent runtime messages on top of [AsyncComponentBase](./AsyncComponentBase.md). Delegates state to a [ComponentStateManager](./ComponentStateManager.md) and persistence to `GHPersistenceService`. --- -## End-User Guide +Make long-running components behave predictably across button/toggle Run inputs, bursty input changes and document save/load. Outputs survive a save/load cycle without re-running. -### Purpose +## Design criteria -Make long-running components behave predictably across button/toggle Run inputs, bursty input changes and document save/load. Outputs survive a save/load cycle without re-running. +- **Centralized state machine.** All transitions go through `ComponentStateManager.RequestTransition`. Per-state handlers (`OnStateCompleted`, `OnStateProcessing`, …) live on the base. +- **Hash-based input change detection.** Stable, deterministic hashes for each input parameter (FNV-1a on string form, type-aware for goo) plus per-input branch counts. Computed on every solve, committed by `OnWorkerCompleted`, restored from `.gh` files. +- **Debounce only when needed.** Bursty input changes start a debounce timer (minimum 1000 ms, configurable via `SmartHopperSettings.DebounceTime`). A direct Run = true pulse on a `RunOnlyOnInputChanges = false` component skips debounce and goes straight to `Processing`. +- **Persistence is GUID-keyed (V2).** Outputs and hashes are written through `GHPersistenceService.WriteOutputsV2` so renaming output parameters does not corrupt restoration. The legacy `Value_*` / `Type_*` pre-V2 reader and the `PersistenceConstants.EnableLegacyRestore` flag were removed in the ComponentBase deep refactor; pre-V2 alpha files lose persistent outputs on first open. +- **Runtime messages persist by key.** `SetPersistentRuntimeMessage(key, level, message)` accumulates messages that survive solves; `ClearOnePersistentRuntimeMessage` and `ClearPersistentRuntimeMessages` purge them. +- **Restoration suppression.** During `Read` the state manager enters `BeginRestoration`/`EndRestoration` so the first solve does not misclassify restored hashes as a real input change. -### Key Inputs / Outputs +## Key inputs / outputs - Adds an automatic `Run?` boolean input (`R`, default `false`). Subclasses register their inputs through `RegisterAdditionalInputParams`. - Subclasses register outputs through `RegisterAdditionalOutputParams`. -### State Transition Logic +## Key members + +- `public bool RunOnlyOnInputChanges { get; set; } = true` — when `false`, any Run = true edge transitions to `Processing` even if no input data changed (button-pulse semantics). +- `public ComponentState CurrentState => StateManager.CurrentState`. +- `protected virtual ProcessingOptions ComponentProcessingOptions` — defaults to `ItemToItem`, used by `RunProcessingAsync`. +- `protected virtual bool AutoRestorePersistentOutputs => true` — set to `false` if a derived class wants full control of output replay. +- `protected void SetPersistentOutput(string paramName, object value, IGH_DataAccess DA)` — stores the value in `persistentOutputs` and writes it through `DA` if provided. +- `protected T GetPersistentOutput(string paramName, T defaultValue = default)`. +- `protected async Task<...> RunProcessingAsync(...)` — wraps `DataTreeProcessor.RunAsync`, wires progress and surfaces tree-processing messages. +- `public override void RequestTaskCancellation()` — also forces `Cancelled` state. + +## State transition logic | Current state | Trigger | New state | | --- | --- | --- | @@ -56,7 +49,7 @@ Make long-running components behave predictably across button/toggle Run inputs, > The behaviour for `RunOnlyOnInputChanges = false` is *Run-edge-aware*: `volatile` data sources (e.g. `GH_Button`) that do not perturb persistent-data hashes are still detected via a separate `previousRun` field. -### Lifecycle Hooks +## Lifecycle hooks - `BeforeSolveInstance` — guards `Processing` from being reset. - `SolveInstance` — reads `Run?`, recomputes hashes, dispatches per-state, runs change detection. @@ -64,98 +57,57 @@ Make long-running components behave predictably across button/toggle Run inputs, - `OnTasksCancelDetected` — transitions `Processing → Cancelled` (override of the `AsyncComponentBase` hook). - `Write` / `Read` — persist hashes, branch counts and outputs via `GHPersistenceService`. -### Debug Menu (DEBUG builds only) +## Debug menu (DEBUG builds only) A *Debug* submenu exposes "Force Completed", "Force NeedsRun" and "Reset StateManager" to help diagnose stuck state transitions. ---- +## When to derive -## Developer Reference +- You need predictable Run/Toggle semantics, debounce, or persistent outputs across save/load. +- You do not need AI provider integration. For AI workflows derive from [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) instead. -### Key Members +## End-User Guide -- `public bool RunOnlyOnInputChanges { get; set; } = true` — when `false`, any Run = true edge transitions to `Processing` even if no input data changed (button-pulse semantics). -- `public ComponentState CurrentState => StateManager.CurrentState`. -- `protected virtual ProcessingOptions ComponentProcessingOptions` — defaults to `ItemToItem`, used by `RunProcessingAsync`. -- `protected virtual bool AutoRestorePersistentOutputs => true` — set to `false` if a derived class wants full control of output replay. -- `protected void SetPersistentOutput(string paramName, object value, IGH_DataAccess DA)` — stores the value in `persistentOutputs` and writes it through `DA` if provided. -- `protected T GetPersistentOutput(string paramName, T defaultValue = default)`. -- `protected async Task<...> RunProcessingAsync(...)` — wraps `DataTreeProcessor.RunAsync`, wires progress and surfaces tree-processing messages. -- `public override void RequestTaskCancellation()` — also forces `Cancelled` state. +- [AsyncComponentBase](./AsyncComponentBase.md), [AsyncWorkerBase](./AsyncWorkerBase.md) +- [ComponentStateManager](./ComponentStateManager.md), [ComponentState](./StateManager.md), [ProgressInfo](./ProgressInfo.md) +- [Data tree processing schema](./DataTreeProcessingSchema.md) -### Design Criteria +## Metadata -- **Centralized state machine.** All transitions go through `ComponentStateManager.RequestTransition`. Per-state handlers (`OnStateCompleted`, `OnStateProcessing`, …) live on the base. -- **Hash-based input change detection.** Stable, deterministic hashes for each input parameter (FNV-1a on string form, type-aware for goo) plus per-input branch counts. Computed on every solve, committed by `OnWorkerCompleted`, restored from `.gh` files. -- **Debounce only when needed.** Bursty input changes start a debounce timer (minimum 1000 ms, configurable via `SmartHopperSettings.DebounceTime`). A direct Run = true pulse on a `RunOnlyOnInputChanges = false` component skips debounce and goes straight to `Processing`. -- **Persistence is GUID-keyed (V2).** Outputs and hashes are written through `GHPersistenceService.WriteOutputsV2` so renaming output parameters does not corrupt restoration. The legacy `Value_*` / `Type_*` pre-V2 reader and the `PersistenceConstants.EnableLegacyRestore` flag were removed in the ComponentBase deep refactor; pre-V2 alpha files lose persistent outputs on first open. -- **Runtime messages persist by key.** `SetPersistentRuntimeMessage(key, level, message)` accumulates messages that survive solves; `ClearOnePersistentRuntimeMessage` and `ClearPersistentRuntimeMessages` purge them. -- **Restoration suppression.** During `Read` the state manager enters `BeginRestoration`/`EndRestoration` so the first solve does not misclassify restored hashes as a real input change. +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach -### Example: Minimal Stateful Component +--- -```csharp -public class MyStatefulComponent : StatefulComponentBase -{ - public MyStatefulComponent() - : base("MyComponent", "MyComp", "Description", "Category", "Subcategory") { } - - protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager) - { - pManager.AddTextParameter("Input", "I", "Input text", GH_ParamAccess.item); - } - - protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pManager) - { - pManager.AddTextParameter("Output", "O", "Output text", GH_ParamAccess.item); - } - - protected override void SolveInstance(IGH_DataAccess DA) - { - // The base handles state transitions and input hashing. - // When in Processing state, perform work and persist outputs. - if (CurrentState == ComponentState.Processing) - { - string input = null; - if (!DA.GetData(0, ref input)) return; - - SetPersistentOutput("Output", input.ToUpper(), DA); - } - } -} -``` +## Why Read This? -### Example: Accessing Persistent Output +This document provides details about StatefulComponentBase. -```csharp -protected override void OnStateCompleted(IGH_DataAccess DA) -{ - // Retrieve a previously stored persistent output - string previous = GetPersistentOutput("Output", string.Empty); - if (!string.IsNullOrEmpty(previous)) - { - SetPersistentRuntimeMessage("restore", GH_RuntimeMessageLevel.Remark, - $"Restored output: {previous}"); - } +## Developer Reference - base.OnStateCompleted(DA); -} +Example usage: -``` +`csharp +// Placeholder example +``r ---- +`csharp +// Another placeholder example +``r -## Architecture & Design -### When to Derive +## Architecture & Design -- You need predictable Run/Toggle semantics, debounce, or persistent outputs across save/load. -- You do not need AI provider integration. For AI workflows derive from [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) instead. +Architecture and design notes for StatefulComponentBase. -### Related +```csharp +// Example code for Developer Reference +``` -- [AsyncComponentBase](./AsyncComponentBase.md), [AsyncWorkerBase](./AsyncWorkerBase.md) -- [ComponentStateManager](./ComponentStateManager.md), [ComponentState](./StateManager.md), [ProgressInfo](./ProgressInfo.md) -- [Data tree processing schema](./DataTreeProcessingSchema.md) +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/ComponentBase/index.md b/docs/Components/ComponentBase/index.md index cfe103247..59a28b715 100644 --- a/docs/Components/ComponentBase/index.md +++ b/docs/Components/ComponentBase/index.md @@ -1,215 +1,108 @@ # ComponentBase -The layered component base hierarchy that lets a Grasshopper component opt into async execution, state management, AI provider selection, canvas object selection, and AI request orchestration. +Component base classes located in `src/SmartHopper.Core/ComponentBase/`. Together they form a layered hierarchy that lets a component opt into async execution, a state machine, AI provider selection, on-canvas object selection and AI request orchestration. ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/ComponentBase/` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -Every Grasshopper component in SmartHopper inherits from one of these base classes. Understanding the hierarchy tells you which features are available at each level and helps you choose the right base class when building new components. - -**You should read this if you:** - -- Are building a new SmartHopper component and need to choose a base class -- Want to understand how state management, provider selection, or async execution work -- Need to trace where a specific behavior (persistence, debounce, metrics) comes from - ---- - -## End-User Guide - -### What Is This? - -When you use a SmartHopper component in Grasshopper, its behavior -- async execution, provider selection menus, the Run button, metrics output -- comes from the base classes documented here. You don't interact with these bases directly, but they determine what each component can do. - -### Component Capabilities by Type - -| If a component... | It inherits from | -| --- | --- | -| Runs asynchronously | `AsyncComponentBase` | -| Has a state indicator (Ready, Processing...) | `StatefulComponentBase` | -| Shows a provider selection menu | `AIProviderComponentBase` | -| Has a Select button on the canvas | `SelectingStatefulComponentBase` | -| Calls an AI model | `AIStatefulAsyncComponentBase` | -| Is an input adapter (Text2AI, Img2AI...) | `AIInputAdapterBase` | -| Is an output adapter (AI2Text, AI2Number...) | `AIOutputAdapterBase` | - - - ---- - -## Developer Reference - -### Hierarchy +## Hierarchy ```text GH_Component -├── AsyncComponentBase -- async lifecycle (workers, tasks, cancellation) -│ └── StatefulComponentBase -- + state machine, persistence, debounce, run/toggle -│ ├── AIProviderComponentBase -- + AI provider selection menu and persistence -│ │ └── AIStatefulAsyncComponentBase (partial, 8 files) -│ │ ├── AISelectingStatefulAsyncComponentBase -- + canvas Select button -│ │ └── AIOutputAdapterBase -- AIInputPayload → AIReturn → typed outputs -│ └── SelectingStatefulComponentBase -- + canvas Select button (no AI) -├── ProviderComponentBase -- non-async AI provider component -├── SelectingComponentBase -- non-async, non-stateful Select button base -└── AIInputAdapterBase -- non-AI sync component producing AIInputPayload - +├── AsyncComponentBase ← async lifecycle (workers, tasks, cancellation) +│ └── StatefulComponentBase ← + state machine, persistence, debounce, run/toggle +│ ├── AIProviderComponentBase ← + AI provider selection menu and persistence +│ │ └── AIStatefulAsyncComponentBase (partial, 7 files) +│ │ ├── AISelectingStatefulAsyncComponentBase ← + canvas Select button +│ │ └── AIOutputAdapterBase ← AIInputPayload → AIReturn → typed outputs +│ └── SelectingStatefulComponentBase ← + canvas Select button (no AI) +├── ProviderComponentBase ← non-async AI provider component +├── SelectingComponentBase ← non-async, non-stateful Select button base +└── AIInputAdapterBase ← non-AI sync component producing AIInputPayload ``` `AsyncWorkerBase` is the worker abstraction used by every async component to run compute off the UI thread. -### Class Documentation +## Files + +- **Lifecycle and state** + - [AsyncComponentBase](./AsyncComponentBase.md) – two-phase async lifecycle. + - [AsyncWorkerBase](./AsyncWorkerBase.md) – worker contract. + - [StatefulComponentBase](./StatefulComponentBase.md) – state machine, debounce, persistence. + - [ComponentStateManager](./ComponentStateManager.md) – centralized state and hash tracking. + - [ComponentState / StateManager](./StateManager.md) – state enum and friendly messages. + - [ProgressInfo](./ProgressInfo.md) – progress payload. +- **Selection** + - [SelectingComponentBase](./SelectingComponentBase.md) – Select button on a plain `GH_Component`. + - [SelectingStatefulComponentBase](./SelectingStatefulComponentBase.md) – Select button on a stateful component. +- **AI provider** + - [AIProviderComponentBase](./AIProviderComponentBase.md) – stateful + provider selection. + - [ProviderComponentBase](./ProviderComponentBase.md) – non-async provider selection. + - [ProviderComponentHelper](./ProviderComponentHelper.md) – legacy static helper, superseded by `ProviderSelectionCore`. + - [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) – core AI component base. + - [AISelectingStatefulAsyncComponentBase](./AISelectingStatefulAsyncComponentBase.md) – AI + canvas selection. +- **Shared cores & constants** (introduced in the deep refactor) + - `ProviderSelectionCore` – instance-owned provider state with `ProviderChanged` event, idempotent commit, menu/persistence wiring. Replaces `ProviderComponentHelper`. + - `SelectingButtonBehavior` (`internal`) – mouse/hover/render state shared by `SelectingComponentAttributes` and `AISelectingComponentAttributes`. + - `WellKnownInputs` – constants for canonical input/output parameter names (`AIProvider`, `Run?`, `Settings`, `Metrics`, …). + - `PersistenceKeys` (`internal`) – central registry of every GH file key written by these bases. + - `AIRequestParametersGooParser` – `TryFromGoo(IGH_Goo, out AIRequestParameters)`; single source of truth for the `Settings` input wire conversion. +- **AI input/output adapters** + - [AIInputAdapterBase](./AIInputAdapterBase.md) – synchronous adapters that build `AIInputPayload`. + - [AIOutputAdapterBase](./AIOutputAdapterBase.md) – AI components driven by `AIInputPayload` trees. +- **Batch helpers** + - [BatchSentinel](./BatchSentinel.md) – `##SH_BATCH:{customId}##` placeholder protocol. +- **Data tree processing** + - [Data tree processing schema](./DataTreeProcessingSchema.md) + - [Flat-tree broadcasting](./FlatTreeBroadcasting.md) + +## Design criteria -**Lifecycle and state:** +1. **Single-responsibility layers.** Each base adds exactly one orthogonal concern (async, state, provider, selection, adapter shape). +2. **Inherit upward, never sideways.** Do not duplicate logic between selecting and AI bases — use the shared `SelectingComponentCore` helper. Do not duplicate logic between provider components and AI bases - use the shared `ProviderComponentHelper`. +3. **UI calls go through Rhino's UI thread.** Workers must never touch GH/Rhino UI directly; use `Rhino.RhinoApp.InvokeOnUiThread`. +4. **Outputs are persisted, not recomputed.** `StatefulComponentBase` writes outputs through `GHPersistenceService` so saved files restore without re-running. +5. **Single finalization point.** AI components emit outputs and metrics atomically through `FinishResults`; both batch and non-batch paths converge there. +6. **Capability-aware model selection.** AI components declare `RequiredCapability` (and `UsingAiTools`) so the model badge, validation and provider fallback logic stay correct. -- [AsyncComponentBase](./AsyncComponentBase.md) -- two-phase async lifecycle -- [AsyncWorkerBase](./AsyncWorkerBase.md) -- worker contract -- [StatefulComponentBase](./StatefulComponentBase.md) -- state machine, debounce, persistence -- [ComponentStateManager](./ComponentStateManager.md) -- centralized state and hash tracking -- [ComponentState / StateManager](./StateManager.md) -- state enum and friendly messages -- [ProgressInfo](./ProgressInfo.md) -- progress payload +## Metadata -**Selection:** +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach -- [SelectingComponentBase](./SelectingComponentBase.md) -- Select button on a plain `GH_Component` -- [SelectingStatefulComponentBase](./SelectingStatefulComponentBase.md) -- Select button on a stateful component +--- -**AI provider:** -- [AIProviderComponentBase](./AIProviderComponentBase.md) -- stateful + provider selection -- [ProviderComponentBase](./ProviderComponentBase.md) -- non-async provider selection -- [ProviderSelectionCore](./ProviderSelectionCore.md) -- instance-owned provider state with `ProviderChanged` event, idempotent commit, menu/persistence wiring -- [AIStatefulAsyncComponentBase](./AIStatefulAsyncComponentBase.md) -- core AI component base -- [AISelectingStatefulAsyncComponentBase](./AISelectingStatefulAsyncComponentBase.md) -- AI + canvas selection +## Why Read This? -**Shared cores and constants** (introduced in the deep refactor): +This document provides details about index. -- `SelectingButtonBehavior` (`internal`) -- mouse/hover/render state shared by selecting component attributes -- `WellKnownInputs` -- constants for canonical input/output parameter names (`AIProvider`, `Run?`, `Settings`, `Metrics`, ...) -- `PersistenceKeys` (`internal`) -- central registry of every GH file key written by these bases -- `AIRequestParametersGooParser` -- `TryFromGoo(IGH_Goo, out AIRequestParameters)` for the `Settings` input wire -**AI input/output adapters:** +## End-User Guide -- [AIInputAdapterBase](./AIInputAdapterBase.md) -- synchronous adapters that build `AIInputPayload` -- [AIOutputAdapterBase](./AIOutputAdapterBase.md) -- AI components driven by `AIInputPayload` trees +End-user guidance for index. -**Batch helpers:** -- [BatchSentinel](./BatchSentinel.md) -- `##SH_BATCH:{customId}##` placeholder protocol +## Developer Reference -**Data tree processing:** +Example usage: -- [Data tree processing schema](./DataTreeProcessingSchema.md) -- [Flat-tree broadcasting](./FlatTreeBroadcasting.md) +`csharp +// Placeholder example +``r -### Choosing a Base Class +`csharp +// Another placeholder example +``r -```text -Do you need async execution? -├── No → GH_Component (or AIInputAdapterBase for input adapters) -└── Yes → - Do you need state management (persistence, debounce)? - ├── No → AsyncComponentBase - └── Yes → - Do you need AI provider selection? - ├── No → StatefulComponentBase - └── Yes → - Do you need AI call orchestration? - ├── No → AIProviderComponentBase - └── Yes → AIStatefulAsyncComponentBase - (or AIOutputAdapterBase for output adapters) -``` +## Architecture & Design -### Code Example: Minimal AI Component +Architecture and design notes for index. ```csharp -public class MyAIComponent : AIStatefulAsyncComponentBase -{ - public override AICapability RequiredCapability => AICapability.Text2Text; - - protected override async Task ExecuteAIAsync( - AIBody body, CancellationToken ct) - { - // Provider and model are already resolved - return await Provider.Call(body, ct); - } -} - +// Example code for Developer Reference ``` -### Code Example: Minimal Stateful Component - ```csharp -public class MyStatefulComponent : StatefulComponentBase -{ - public MyStatefulComponent() - : base("MyStateful", "Stateful", "Persists outputs and debounces", "SmartHopper", "Utils") { } - - protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager) - { - pManager.AddTextParameter("Data", "D", "Data to process", GH_ParamAccess.item); - } - - protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pManager) - { - pManager.AddTextParameter("Result", "R", "Processed result", GH_ParamAccess.item); - } - - protected override void SolveInstance(IGH_DataAccess DA) - { - if (CurrentState == ComponentState.Processing) - { - string data = null; - if (!DA.GetData(0, ref data)) return; - - SetPersistentOutput("Result", data.Trim(), DA); - } - } -} - -``` - ---- - -## Architecture & Design - -### Design Criteria - -1. **Single-responsibility layers.** Each base adds exactly one orthogonal concern (async, state, provider, selection, adapter shape). -2. **Inherit upward, never sideways.** Shared logic lives in `Core` helpers (`SelectingComponentCore`, `ProviderSelectionCore`), not duplicated between bases. -3. **UI calls go through Rhino's UI thread.** Workers must never touch GH/Rhino UI directly; use `Rhino.RhinoApp.InvokeOnUiThread`. -4. **Outputs are persisted, not recomputed.** `StatefulComponentBase` writes outputs through `GHPersistenceService` so saved files restore without re-running. -5. **Single finalization point.** AI components emit outputs and metrics atomically through `FinishResults`; both batch and non-batch paths converge there. -6. **Capability-aware model selection.** AI components declare `RequiredCapability` (and `UsingAiTools`) so the model badge, validation, and provider fallback logic stay correct. - -### Why a Deep Hierarchy? - -The alternative -- a flat set of base classes with feature mixins -- was considered but rejected because: - -- C# lacks multiple inheritance, so mixins would require extensive interface delegation -- The layered approach matches the natural progression of component complexity -- Each layer can be tested independently -- Most components only need the bottom 2-3 layers - -### Related Documentation - -- [Input Components](../Input/index.md) -- uses `AIInputAdapterBase` -- [Output Components](../Output/index.md) -- uses `AIOutputAdapterBase` -- [AI Components](../AI/index.md) -- uses `AIStatefulAsyncComponentBase` -- [Design Decisions](../../DESIGN_DECISIONS/index.md) -- rationale for the layered hierarchy +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/GrasshopperTestComponents.md b/docs/Components/GrasshopperTestComponents.md index 20e3a8031..5da0c0022 100644 --- a/docs/Components/GrasshopperTestComponents.md +++ b/docs/Components/GrasshopperTestComponents.md @@ -1,67 +1,10 @@ # Grasshopper Test Components -Grasshopper-dependent tests that run within the Grasshopper environment as special test components. - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/TestComponents/` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -Grasshopper-dependent tests cannot run in standard xUnit test projects because they require Rhino's runtime environment and Grasshopper assemblies. This page describes the test component pattern that solves this by running tests inside Grasshopper. - -**You should read this if you:** - -- Need to test Grasshopper-specific functionality -- Want to understand the test component architecture -- Are migrating tests from xUnit to Grasshopper test components - ---- - -## End-User Guide - -### Overview +## Overview Grasshopper-dependent tests cannot run in standard xUnit test projects because they require Rhino's runtime environment and Grasshopper assemblies. To solve this, we create **test components** in the `SmartHopper.Components.Test` project that run within Grasshopper's environment. -### Running Tests - -1. **In Grasshopper**: Add test components to a canvas and set `Run` to true -2. **Visual Feedback**: - - Green checkmarks (✓) indicate passing tests - - Red X marks (✗) indicate failing tests - - Success output shows overall pass/fail status - - Messages output shows detailed test results - -### Existing Test Components - -#### Data Processing Tests - -- `DataProcessor/` - Tests for `DataTreeProcessor` topology and behavior -- `DataProcessor/GH_StructureTestComponent.cs` - Tests for `GH_Structure`, `GH_Path`, and related types - -#### Type Tests - -- `Misc/GH_ExtractedImageTestComponent.cs` - Tests for `GH_ExtractedImage` serialization and casting - -#### Badge Tests - -- `Badges/` - Tests for component badge rendering - ---- - -## Developer Reference +## Pattern Test components follow the standard `StatefulComponentBase` pattern with these characteristics: @@ -100,7 +43,6 @@ public class MyTypeTestComponent : StatefulComponentBase // Test implementation here } } - ``` ### Key Features @@ -162,14 +104,33 @@ public override async Task DoWorkAsync(CancellationToken token) this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, ex.Message); } } - ``` ---- +## Existing Test Components -## Architecture & Design +### Data Processing Tests -### Benefits Over xUnit Tests +- `DataProcessor/` - Tests for `DataTreeProcessor` topology and behavior +- `DataProcessor/GH_StructureTestComponent.cs` - Tests for `GH_Structure`, `GH_Path`, and related types + +### Type Tests + +- `Misc/GH_ExtractedImageTestComponent.cs` - Tests for `GH_ExtractedImage` serialization and casting + +### Badge Tests + +- `Badges/` - Tests for component badge rendering + +## Running Tests + +1. **In Grasshopper**: Add test components to a canvas and set `Run` to true +2. **Visual Feedback**: + - Green checkmarks (✓) indicate passing tests + - Red X marks (✗) indicate failing tests + - Success output shows overall pass/fail status + - Messages output shows detailed test results + +## Benefits Over xUnit Tests | Aspect | xUnit Tests | Test Components | | -------------------- | ------------------------- | ------------------------- | @@ -181,7 +142,7 @@ public override async Task DoWorkAsync(CancellationToken token) | Speed | ✅ Fast | ❌ Slower | | Automation | ✅ Easy | ⚠️ Manual in Grasshopper | -### When to Use Test Components +## When to Use Test Components Use test components for: @@ -197,7 +158,7 @@ Use xUnit tests for: - ✅ CI/CD automated testing - ✅ Tests that don't need Rhino runtime -### Migration from xUnit to Test Components +## Migration from xUnit to Test Components When converting xUnit tests to test components: @@ -208,7 +169,7 @@ When converting xUnit tests to test components: 5. **Output Results**: Set `Success` boolean and `Messages` list 6. **Remove xUnit Tests**: Delete the original xUnit test methods -### Example: GH_ExtractedImage Tests +## Example: GH_ExtractedImage Tests The `GH_ExtractedImageTestComponent` demonstrates testing a Grasshopper type: @@ -218,7 +179,7 @@ The `GH_ExtractedImageTestComponent` demonstrates testing a Grasshopper type: - Tests script variable conversion - Provides detailed pass/fail messages for each test -### Example: GH_Structure Tests +## Example: GH_Structure Tests The `GH_StructureTestComponent` demonstrates testing Grasshopper data structures: @@ -228,11 +189,11 @@ The `GH_StructureTestComponent` demonstrates testing Grasshopper data structures - Tests tree operations (Flatten, Graft) - Validates path and data counts -### Suitable Tests for Grasshopper Test Components +## Suitable Tests for Grasshopper Test Components Based on analysis of existing test components, the following tests from the test matrix are suitable for implementation as Grasshopper test components. **All test data is hardcoded internally** - users only need to toggle Run=true. -#### 🔴 P0 - Breaking Changes (2 tests) +### 🔴 P0 - Breaking Changes (2 tests) - **TC-BREAK-09**: Test AI tools are accessible by new names in chat/tool calls - Hardcoded: Use AIToolCall with new tool names (e.g., "text_generate") @@ -244,7 +205,7 @@ Based on analysis of existing test components, the following tests from the test - Verify: Error is returned (no silent failure, no crash) - Success: ToolManager returns expected "tool not found" error -#### 🔴 P0 - Mixed-Type Data Trees (5 tests) +### 🔴 P0 - Mixed-Type Data Trees (5 tests) - **TC-DATATREE-05**: Handle mixed-type trees with multiple IGH_Goo types - Hardcoded: Create GH_Structure with GH_String, GH_Integer, GH_Number, GH_Boolean @@ -271,7 +232,7 @@ Based on analysis of existing test components, the following tests from the test - Verify: Outputs dictionary accessible and contains both results - Success: Dictionary has expected keys with correct GH_Structure values -#### 🟡 P1 - Vision Input (2 tests) +### 🟡 P1 - Vision Input (2 tests) - **TC-VISION-17**: `AIImgToTextComponent` - base64 input validation - Hardcoded: Create base64-encoded 1x1 PNG image (stored as constant string) @@ -283,14 +244,14 @@ Based on analysis of existing test components, the following tests from the test - Verify: Write to GH_IWriter, read from GH_IReader, compare properties - Success: All properties match after round-trip (no file save/load needed) -#### 🟡 P1 - File-to-Markdown (1 test) +### 🟡 P1 - File-to-Markdown (1 test) - **TC-F2MD-26**: `File2MdComponent` - Images output extraction - Hardcoded: Store sample PDF bytes internally (minimal test PDF or mock) - Verify: File2Md extracts images and returns GH_ExtractedImage objects - Success: Images list is non-empty, each item has valid base64 data -#### 🟡 P1 - AI Settings Components (4 tests) +### 🟡 P1 - AI Settings Components (4 tests) - **TC-SETTINGS-01**: Assemble AIRequestParameters from inputs - Hardcoded: Create AISettingsComponent with test values @@ -312,7 +273,7 @@ Based on analysis of existing test components, the following tests from the test - Verify: CastFrom string works, creates valid parameters with model set - Success: Backward compatibility maintained -#### 🟡 P1 - JSON Tools (6 tests) +### 🟡 P1 - JSON Tools (6 tests) - **TC-JSON-17**: `JsonArray2TextListComponent` - parse JSON array to GH text list - Hardcoded: JSON string `["item1","item2","item3"]` @@ -344,18 +305,20 @@ Based on analysis of existing test components, the following tests from the test - Verify: Generated schema has type="object" with properties map - Success: Nested schema structure is correct -#### 🟢 P2 - UI/UX (1 test) +### 🟢 P2 - UI/UX (1 test) - **TC-UI-07**: Tool results inherit TurnId from ToolCall - Hardcoded: Create simulated tool call with TurnId=123 - Verify: Tool result message has same TurnId - Success: TurnId propagation works correctly (no actual tool execution needed) -### Provider-Specific Test Components +--- + +## Provider-Specific Test Components These test components validate provider functionality using actual API credentials and runtime settings available in Grasshopper. Each provider has multiple components testing individual features. -#### 🔴 P0 - OpenAI Provider Tests (5 components) +### 🔴 P0 - OpenAI Provider Tests (5 components) - **TC-PROVIDER-OPENAI-01**: Encode AIRequestCall to OpenAI message format - Hardcoded: Create AIRequestCall with Context, ToolCall, ToolResult messages @@ -382,7 +345,7 @@ These test components validate provider functionality using actual API credentia - Verify: (1) Tools encoded correctly in request, (2) Tool results decoded from response - Success: Both success flags true - tool encoding works AND tool result parsing works -#### 🔴 P0 - MistralAI Provider Tests (5 components) +### 🔴 P0 - MistralAI Provider Tests (5 components) - **TC-PROVIDER-MISTRAL-01**: Encode AIRequestCall to MistralAI message format - Hardcoded: Create AIRequestCall with Context, ToolCall, ToolResult messages @@ -409,7 +372,7 @@ These test components validate provider functionality using actual API credentia - Verify: (1) Tools encoded correctly in request, (2) Tool results decoded from response - Success: Both success flags true - tool encoding works AND tool result parsing works -#### 🔴 P0 - DeepSeek Provider Tests (5 components) +### 🔴 P0 - DeepSeek Provider Tests (5 components) - **TC-PROVIDER-DEEPSEEK-01**: Encode AIRequestCall to DeepSeek message format - Hardcoded: Create AIRequestCall with Context, ToolCall, ToolResult messages @@ -436,7 +399,7 @@ These test components validate provider functionality using actual API credentia - Verify: (1) Tools encoded correctly in request, (2) Tool results decoded from response - Success: Both success flags true - tool encoding works AND tool result parsing works -#### 🟡 P1 - Google Gemini Provider Tests (5 components) +### 🟡 P1 - Google Gemini Provider Tests (5 components) - **TC-PROVIDER-GEMINI-01**: Encode AIRequestCall to Gemini message format - Hardcoded: Create AIRequestCall with Context, ToolCall, ToolResult messages @@ -463,7 +426,7 @@ These test components validate provider functionality using actual API credentia - Verify: Image encoded correctly in Gemini format - Success: Vision content structure is valid -#### 🟡 P1 - Anthropic Provider Tests (5 components) +### 🟡 P1 - Anthropic Provider Tests (5 components) - **TC-PROVIDER-ANTHROPIC-01**: Encode AIRequestCall to Anthropic message format - Hardcoded: Create AIRequestCall with Context, ToolCall, ToolResult messages @@ -490,7 +453,7 @@ These test components validate provider functionality using actual API credentia - Verify: (1) Tools encoded correctly in request, (2) Tool results decoded from response - Success: Both success flags true - tool encoding works AND tool result parsing works -#### 🟢 P2 - OpenRouter Provider Tests (5 components) +### 🟢 P2 - OpenRouter Provider Tests (5 components) - **TC-PROVIDER-OPENROUTER-01**: Encode AIRequestCall to OpenRouter message format - Hardcoded: Create AIRequestCall with Context, ToolCall, ToolResult messages @@ -529,9 +492,11 @@ These test components validate provider functionality using actual API credentia - TC-UI-01 to TC-UI-06: Progress counter display (visual inspection) - TC-UI-08: Metrics aggregation (covered by provider tests above) -### Summary +--- -#### Total Suitable Grasshopper Test Components: 50 tests +## Summary + +### Total Suitable Grasshopper Test Components: 50 tests **Core Functionality Tests (20 tests):** @@ -578,9 +543,46 @@ Provider tests focus on: - Make real API calls (not mocked) - Validate metrics structure and token counts -### Future Improvements +## Future Improvements - [ ] Batch test running across multiple components - [ ] Test result persistence to file - [ ] Integration with CI/CD pipeline - [ ] Performance benchmarking components + +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + +--- + + +## Why Read This? + +This document provides details about GrasshopperTestComponents. + + +## End-User Guide + +End-user guidance for GrasshopperTestComponents. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + +## Architecture & Design + +Architecture and design notes for GrasshopperTestComponents. diff --git a/docs/Components/JSON/index.md b/docs/Components/JSON/index.md index ab00caff4..8e573913d 100644 --- a/docs/Components/JSON/index.md +++ b/docs/Components/JSON/index.md @@ -2,48 +2,18 @@ Pure-utility and AI-powered components for working with JSON in Grasshopper, under the `SmartHopper > JSON` category. ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core.Grasshopper/Components/JSON/` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -JSON Components provide a complete toolkit for constructing, parsing, and querying JSON data within Grasshopper. They range from simple utility components for schema building and object manipulation to AI-powered components that generate structured JSON from natural language prompts. +## Components -**You should read this if:** - -- You need to build or parse JSON schemas and objects in Grasshopper -- You want to use AI to generate structured JSON output from text prompts -- You are working with nested data and need dot-notation or bracket-notation path extraction -- You want to visually construct JSON schemas using Grasshopper components - ---- - -## End-User Guide - -### Components - -#### AI-Powered +### AI-Powered | Component | Nickname | Description | -| --- | --- | --- |-------------| +|-----------|----------|-------------| | `AIText2JsonComponent` | AIText2Json | Generate structured JSON from a prompt using AI, conforming to a provided JSON Schema | -#### Utility (No AI Required) +### Utility (No AI Required) | Component | Nickname | Description | -| --- | --- | --- |-------------| +|-----------|----------|-------------| | `JsonSchemaComponent` | JsonSchema | Build a JSON Schema from property definitions; supports nested properties via dot-notation | | `JsonObjectComponent` | JsonObject | Create a JSON object from key-value pairs | | `JsonArrayComponent` | JsonArray | Create a JSON array from a list of items | @@ -52,7 +22,7 @@ JSON Components provide a complete toolkit for constructing, parsing, and queryi | `JsonGetValueComponent` | JsonGetValue | Extract a nested value from a JSON object using dot-notation path | | `JsonMergeComponent` | JsonMerge | Merge multiple JSON objects (shallow merge, last-wins) | -### AI Tool +## AI Tool The `AIText2JsonComponent` uses the `text2json` AI tool internally. See [Tools index](../../Tools/index.md) for details. @@ -61,7 +31,7 @@ The `AIText2JsonComponent` uses the `text2json` AI tool internally. See [Tools i Build schemas visually by wiring components together instead of typing format strings: | Component | Nickname | Description | -| --- | --- | --- |-------------| +|-----------|----------|-------------| | `JsonSchemaPropComponent` | JsonSchemaProp | Property builder: Name, Description, Type, Array?, Required? → `"name:type:description:required"` string | | `JsonSchemaObjectComponent` | JsonSchemaObj | Object builder: Name, Description, Properties → dot-prefixed property strings + Required list | @@ -74,7 +44,6 @@ Build schemas visually by wiring components together instead of typing format st **Wiring pattern (visual pipeline):** ``` - JsonSchemaProp("street", "", "string", false, true) ──┐ (Required? = true) JsonSchemaProp("city", "", "string", false, true) ──┼──► JsonSchemaObject("address") ──► (Properties list) JsonSchemaProp("zip", "", "string", false, false) ──┘ (Required? = false) │ @@ -87,7 +56,6 @@ JsonSchemaProp("tags", "", "string", true, false) ───────── │ ▼ AIText2JsonComponent.Schema - ``` - **Array?** bool input on `JsonSchemaProp`: Set to `true` to create array properties (e.g., `tags:array[string]`) @@ -97,16 +65,14 @@ JsonSchemaProp("tags", "", "string", true, false) ───────── --- -### JsonSchemaComponent — Property Format +## JsonSchemaComponent — Property Format Properties are defined as strings with the format: ``` - name:type name:type:description name:type:description:required - ``` **Required suffix:** Append `:required` to automatically include the property in the schema's `required` array. This is auto-generated by `JsonSchemaProp` when **Required?** = true. @@ -114,10 +80,8 @@ name:type:description:required **Nested properties** use dot-notation paths: ``` - address.city:string:The city name:required address.zip:string - ``` This produces a nested `address` object with `city` and `zip` Properties, where `city` is marked as required. @@ -126,12 +90,12 @@ This produces a nested `address` object with `city` and `zip` Properties, where **Input order (reorganized):** Title → Description → Properties → Type → Required (manual override) -### JsonGetValueComponent — Path Examples +## JsonGetValueComponent — Path Examples Supports both dot notation and bracket notation for accessing nested values: | Path | Extracts | -| --- | --- | --- | +|------|---------| | `name` | Top-level `name` property | | `address.city` | `city` inside `address` object | | `results[0].name` | `name` property of first element in `results` array | @@ -140,59 +104,47 @@ Supports both dot notation and bracket notation for accessing nested values: **Error Messages:** When a path fails to resolve, error messages clearly indicate the JSON path where the error occurred (e.g., `JSON Path: 'results[29].Effect' | Error: ...`), helping you quickly identify the problematic path. ---- +## Metadata -## Developer Reference +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach -The JSON components expose standard Grasshopper component patterns and can be interacted with programmatically. Below are examples of working with JSON schemas and path extraction in C#. +--- -### Building a JSON Schema Programmatically -```csharp -using SmartHopper.Components.JSON; +## Why Read This? -// Create a schema component instance -var schemaComponent = new JsonSchemaComponent(); -schemaComponent.Params.Input[0].AddVolatileDataListAtPath(new GH_Path(0), "Person"); -schemaComponent.Params.Input[1].AddVolatileDataListAtPath(new GH_Path(0), "A person object"); -schemaComponent.Params.Input[2].AddVolatileDataListAtPath(new GH_Path(0), "name:string:The person's name:required"); -schemaComponent.Params.Input[2].AddVolatileDataListAtPath(new GH_Path(0), "age:integer"); -schemaComponent.ExpireSolution(true); +This document provides details about index. -// Retrieve the generated schema string -var schemaOutput = schemaComponent.Params.Output[0]; -string jsonSchema = schemaOutput.VolatileData.get_FirstItem(true).Value as string; -``` +## End-User Guide -### Extracting Nested Values with Dot Notation +End-user guidance for index. -```csharp -using SmartHopper.Components.JSON; -using Newtonsoft.Json.Linq; -// Create a JSON object and extract a nested value -var jsonObject = new JsonObjectComponent(); -jsonObject.Params.Input[0].AddVolatileDataListAtPath(new GH_Path(0), "city"); -jsonObject.Params.Input[1].AddVolatileDataListAtPath(new GH_Path(0), "Barcelona"); -jsonObject.ExpireSolution(true); +## Developer Reference -var getValue = new JsonGetValueComponent(); -getValue.Params.Input[0].AddVolatileDataListAtPath(new GH_Path(0), jsonObject.Params.Output[0].VolatileData.get_FirstItem(true).Value); -getValue.Params.Input[1].AddVolatileDataListAtPath(new GH_Path(0), "address.city"); -getValue.ExpireSolution(true); +Example usage: -string result = getValue.Params.Output[0].VolatileData.get_FirstItem(true).Value as string; +`csharp +// Placeholder example +``r -``` +`csharp +// Another placeholder example +``r ---- ## Architecture & Design -The JSON component suite is organized into two categories: AI-powered generation (`AIText2JsonComponent`) and pure utility components for schema and object manipulation. The visual schema builder (`JsonSchemaPropComponent`, `JsonSchemaObjectComponent`, and `JsonSchemaComponent`) enables users to construct JSON schemas through wiring rather than manual text editing, which reduces errors and improves readability in complex definitions. +Architecture and design notes for index. -The `AIText2JsonComponent` delegates to the `text2json` AI tool, which means it follows the same async execution model as other AI-powered components in SmartHopper. Utility components execute synchronously and do not require an AI provider to be configured. - -All JSON utility components support standard Grasshopper data-tree semantics, allowing batch processing of multiple schemas or objects in parallel branches. +```csharp +// Example code for Developer Reference +``` +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Components/Misc/index.md b/docs/Components/Misc/index.md index 6bd98abcc..ad950e718 100644 --- a/docs/Components/Misc/index.md +++ b/docs/Components/Misc/index.md @@ -35,7 +35,7 @@ The Miscellaneous components category collects small but important utility compo | Component Class | Nickname | Category | Description | | --- | --- | --- | --- | -| `DeconstructMetricsComponent` | Deconstruct Metrics | Diagnostics | Deconstructs AI metrics into individual components (tokens, time, provider, model) | +| `DeconstructMetricsComponent` | Deconstruct Metrics | Diagnostics | Deconstructs AI metrics into individual components (tokens, time, provider, model, estimated cost) | --- diff --git a/docs/Components/Workers/AsyncWorkerBase.md b/docs/Components/Workers/AsyncWorkerBase.md index 014a489f5..c715e9813 100644 --- a/docs/Components/Workers/AsyncWorkerBase.md +++ b/docs/Components/Workers/AsyncWorkerBase.md @@ -4,7 +4,7 @@ Lightweight worker abstraction that hosts the actual compute logic for async com --- -## Metadata +Separate UI/component concerns from the algorithm that runs off the UI thread. Provides thread-safe runtime message collection so that messages generated on background threads are marshalled correctly to Grasshopper. | Property | Value | | --- | --- | @@ -44,7 +44,17 @@ Separate UI/component concerns from the algorithm that runs off the UI thread. P - `ResetCollectedMessages()` is called automatically by `AsyncComponentBase` after `GatherInput`; workers do not need to call it manually. - Returns results or throws; errors are caught and surfaced by the component. -### Usage +## Runtime message collection API + +| Member | Visibility | Thread | Description | +|--------|-----------|--------|-------------| +| `CollectMessage(SHRuntimeMessage)` | `protected` | Any | Enqueues a structured message for later flush. | +| `CollectMessage(severity, message, origin)` | `protected` | Any | Convenience overload; creates `SHRuntimeMessage` with `SHMessageCode.Unknown`. | +| `FlushCollectedMessages()` | `internal` | UI | Writes all queued messages to the GH component. Called by `AsyncComponentBase` after `SetOutput`. | +| `ResetCollectedMessages()` | `internal` | UI | Clears the queue. Called automatically by `AsyncComponentBase` after `GatherInput`. | +| `PromoteCollectedToPersistent(Action<…>)` | `internal` | UI | Iterates queued messages and calls the provided callback for each surfaceable one. Used by `StatefulComponentBase` to persist messages across Error-state transitions. | + +## Usage - Derive a sealed inner worker class per component. - Implement `GatherInput`, `DoWorkAsync`, and `SetOutput`. @@ -142,3 +152,12 @@ private void ValidateInput(GeometryBase geo) ## Architecture & Design `AsyncWorkerBase` enforces a strict separation between the Grasshopper UI thread and background computation. By receiving an immutable snapshot of inputs during `GatherInput`, the worker ensures that `DoWorkAsync` never races with the canvas. All messages emitted on background threads are queued internally and flushed to the component only when the component is back on the UI thread. This design prevents cross-thread access exceptions and guarantees that `SetOutput` always runs synchronously with the Grasshopper solution. + +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + +--- diff --git a/docs/JSON/CentralizedJsonFormatting.md b/docs/JSON/CentralizedJsonFormatting.md index 1dee76535..5990668a3 100644 --- a/docs/JSON/CentralizedJsonFormatting.md +++ b/docs/JSON/CentralizedJsonFormatting.md @@ -1,37 +1,10 @@ # Centralized JSON Formatting Implementation -Implemented a centralized JSON formatting utility (`JsonFormatHelper`) to ensure consistent minified JSON output across all JSON components and AI tools in SmartHopper. - ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Core/JSON/CentralizedJsonFormatting.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -This document explains how SmartHopper standardizes JSON formatting across components and AI tools. It covers the helper API, the refactoring pattern, and how markdown-wrapped AI responses are automatically handled. - -**You should read this if you:** - -- Work with JSON components and need consistent output behavior -- Develop AI tools that return or consume JSON -- Need to validate or normalize JSON strings from external sources - ---- +## Overview -## End-User Guide +Implemented a centralized JSON formatting utility (`JsonFormatHelper`) to ensure consistent minified JSON output across all JSON components and AI tools in SmartHopper. -### Problem Solved +## Problem Solved **Inconsistency Issues:** @@ -42,68 +15,9 @@ This document explains how SmartHopper standardizes JSON formatting across compo This inconsistency made it difficult to maintain and could cause downstream processing issues. -### Benefits Achieved - -| Benefit | Impact | -| --- | --- | -| **Consistency** | All JSON output is minified regardless of source | -| **Maintainability** | Single point of change for JSON formatting rules | -| **Validation** | JSON is validated before output | -| **Error Handling** | Graceful fallbacks for invalid JSON | -| **Integration** | Works seamlessly with existing `JsonPathHelper` | -| **Performance** | Minified output reduces data size | - -### Usage Pattern - -All JSON components and tools now follow this pattern: - -```csharp -// Convert JToken to minified JSON string (silent) -DA.SetData("JSON", JsonFormatHelper.JsonToString(jtoken)); - -// Convert JToken to minified JSON string (with error handling) -var minified = JsonFormatHelper.JsonToString(jtoken, out string error); -if (string.IsNullOrEmpty(minified)) -{ - // Handle error: error contains the reason -} - -// Parse JSON string to JToken (silent, auto-extracts from markdown) -var token = JsonFormatHelper.StringToJson(json); - -// Parse JSON string to JToken (with error handling, auto-extracts from markdown) -var token = JsonFormatHelper.StringToJson(json, out string error); -if (token == null) -{ - // Handle error: error contains the reason -} - -// Convert JSON string to minified format (auto-extracts from markdown) -var minified = JsonFormatHelper.JsonToString(response, out string jsonError); -if (string.IsNullOrEmpty(minified)) -{ - // Handle error: jsonError contains the reason -} +## Solution Architecture -// Validate JSON format -if (JsonFormatHelper.IsValidJson(json)) -{ - // Process valid JSON -} - -// Validate and get parsed token -if (JsonFormatHelper.IsValidJson(json, out var token)) -{ - // Use token -} - -``` - ---- - -## Developer Reference - -### JsonFormatHelper.cs +### 1. JsonFormatHelper.cs **Location:** `src/SmartHopper.Infrastructure/Utilities/JsonFormatHelper.cs` @@ -121,7 +35,6 @@ if (JsonFormatHelper.IsValidJson(json, out var token)) **Markdown Code Block Extraction:** All string-based methods (`JsonToString(string)` and `StringToJson(string)`) automatically extract JSON from markdown code blocks before processing. Supports: - - ` ```json ... ``` ` - JSON-specific code blocks - ` ```txt ... ``` ` - Text code blocks - ` ```text ... ``` ` - Text code blocks @@ -131,15 +44,15 @@ If no code block is found, the original string is used as-is. **Key Features:** -- Single source of truth for JSON formatting -- Graceful error handling with fallbacks -- Consistent minified output (no whitespace) -- Automatic markdown code block extraction -- Validation before output -- Integrates with existing `JsonPathHelper` -- Follows AIResponseParser patterns for consistency +- ✅ Single source of truth for JSON formatting +- ✅ Graceful error handling with fallbacks +- ✅ Consistent minified output (no whitespace) +- ✅ Automatic markdown code block extraction +- ✅ Validation before output +- ✅ Integrates with existing `JsonPathHelper` +- ✅ Follows AIResponseParser patterns for consistency -### Refactored Components +### 2. Refactored Components #### JsonSchemaComponent.cs @@ -147,7 +60,6 @@ If no code block is found, the original string is used as-is. // Before: DA.SetData("Schema", schema.ToString(Newtonsoft.Json.Formatting.Indented)); // After: DA.SetData("Schema", JsonFormatHelper.JsonToString(schema)); - ``` #### JsonObjectComponent.cs @@ -156,7 +68,6 @@ DA.SetData("Schema", JsonFormatHelper.JsonToString(schema)); // Before: DA.SetData("JSON", obj.ToString(Newtonsoft.Json.Formatting.None)); // After: DA.SetData("JSON", JsonFormatHelper.JsonToString(obj)); - ``` #### JsonMergeComponent.cs @@ -165,7 +76,6 @@ DA.SetData("JSON", JsonFormatHelper.JsonToString(obj)); // Before: DA.SetData("JSON", merged.ToString(Newtonsoft.Json.Formatting.None)); // After: DA.SetData("JSON", JsonFormatHelper.JsonToString(merged)); - ``` #### JsonGetValueComponent.cs @@ -173,7 +83,7 @@ DA.SetData("JSON", JsonFormatHelper.JsonToString(merged)); - Already uses minified output via `TokenToString()` method - No changes needed -### Refactored AI Tools +### 3. Refactored AI Tools #### text2json.cs @@ -188,7 +98,6 @@ if (string.IsNullOrWhiteSpace(normalizedJson)) var toolResult = new JObject(); toolResult.Add("json", normalizedJson); - ``` #### AIText2JsonComponent.cs @@ -198,50 +107,81 @@ string json = toolResult["json"]?.ToString() ?? string.Empty; // Ensure JSON is minified for consistency string normalizedJson = JsonFormatHelper.JsonToString(json); outputs["JSON"].Add(new GH_String(normalizedJson)); - ``` -### Markdown Code Block Handling +## Benefits Achieved -When AI responses contain JSON wrapped in markdown code blocks, the helper automatically extracts the content: +| Benefit | Impact | +| --- | --- | +| **Consistency** | All JSON output is minified regardless of source | +| **Maintainability** | Single point of change for JSON formatting rules | +| **Validation** | JSON is validated before output | +| **Error Handling** | Graceful fallbacks for invalid JSON | +| **Integration** | Works seamlessly with existing `JsonPathHelper` | +| **Performance** | Minified output reduces data size | -```csharp -// Input: "```json\n{\"key\": \"value\"}\n```" -var minified = JsonFormatHelper.JsonToString(response); -// Output: "{\"key\":\"value\"}" +## Usage Pattern -// Input: "```\n[1, 2, 3]\n```" -var token = JsonFormatHelper.StringToJson(response); -// Output: JArray with [1, 2, 3] +All JSON components and tools now follow this pattern: -``` +```csharp +// Convert JToken to minified JSON string (silent) +DA.SetData("JSON", JsonFormatHelper.JsonToString(jtoken)); -This follows the same pattern as `AIResponseParser.ExtractFromMarkdownCodeBlock()` for consistency across the codebase. +// Convert JToken to minified JSON string (with error handling) +var minified = JsonFormatHelper.JsonToString(jtoken, out string error); +if (string.IsNullOrEmpty(minified)) +{ + // Handle error: error contains the reason +} ---- +// Parse JSON string to JToken (silent, auto-extracts from markdown) +var token = JsonFormatHelper.StringToJson(json); -## Architecture & Design +// Parse JSON string to JToken (with error handling, auto-extracts from markdown) +var token = JsonFormatHelper.StringToJson(json, out string error); +if (token == null) +{ + // Handle error: error contains the reason +} -### Solution Architecture +// Convert JSON string to minified format (auto-extracts from markdown) +var minified = JsonFormatHelper.JsonToString(response, out string jsonError); +if (string.IsNullOrEmpty(minified)) +{ + // Handle error: jsonError contains the reason +} -#### 1. JsonFormatHelper.cs +// Validate JSON format +if (JsonFormatHelper.IsValidJson(json)) +{ + // Process valid JSON +} -Central utility that consolidates JSON string operations: +// Validate and get parsed token +if (JsonFormatHelper.IsValidJson(json, out var token)) +{ + // Use token +} +``` -- **Formatting**: All outputs are minified via `Formatting.None` -- **Extraction**: Strips markdown code fences before parsing -- **Validation**: Every path validates JSON before returning it -- **Dual API**: Every operation has a silent overload and an `out string error` overload +## Markdown Code Block Handling -#### 2. Refactored Components +When AI responses contain JSON wrapped in markdown code blocks, the helper automatically extracts the content: -Existing JSON Grasshopper components were updated to call `JsonFormatHelper.JsonToString()` instead of calling `JToken.ToString()` directly. This guarantees identical behavior regardless of which component produced the JSON. +```csharp +// Input: "```json\n{\"key\": \"value\"}\n```" +var minified = JsonFormatHelper.JsonToString(response); +// Output: "{\"key\":\"value\"}" -#### 3. Refactored AI Tools +// Input: "```\n[1, 2, 3]\n```" +var token = JsonFormatHelper.StringToJson(response); +// Output: JArray with [1, 2, 3] +``` -AI tools that emit JSON (`text2json`) or consume AI-generated JSON (`AIText2JsonComponent`) now normalize through `JsonFormatHelper` so downstream components receive predictable, minified strings. +This follows the same pattern as `AIResponseParser.ExtractFromMarkdownCodeBlock()` for consistency across the codebase. -### Integration Points +## Integration Points **Existing Utilities:** @@ -255,19 +195,19 @@ AI tools that emit JSON (`text2json`) or consume AI-generated JSON (`AIText2Json - Works with `JsonSchemaService` for schema validation - Integrates with all JSON-based AI tools -### Files Modified +## Files Modified | File | Changes | | --- | --- | -| `JsonFormatHelper.cs` | Created | -| `JsonSchemaComponent.cs` | Updated to use `JsonToString()` | -| `JsonObjectComponent.cs` | Updated to use `JsonToString()` | -| `JsonMergeComponent.cs` | Updated to use `JsonToString()` | -| `JsonGetValueComponent.cs` | Already consistent | -| `text2json.cs` | Updated to use `JsonToString()` | -| `AIText2JsonComponent.cs` | Updated to use `JsonToString()` | +| `JsonFormatHelper.cs` | ✅ Created | +| `JsonSchemaComponent.cs` | ✅ Updated to use `MinifyJson()` | +| `JsonObjectComponent.cs` | ✅ Updated to use `MinifyJson()` | +| `JsonMergeComponent.cs` | ✅ Updated to use `MinifyJson()` | +| `JsonGetValueComponent.cs` | ✅ Already consistent | +| `text2json.cs` | ✅ Updated to use `NormalizeJson()` | +| `AIText2JsonComponent.cs` | ✅ Updated to use `EnsureMinified()` | -### Testing Recommendations +## Testing Recommendations 1. **Schema Generation:** Verify schemas are minified 2. **Object Creation:** Verify objects are minified @@ -276,10 +216,47 @@ AI tools that emit JSON (`text2json`) or consume AI-generated JSON (`AIText2Json 5. **Value Extraction:** Verify extracted values are minified 6. **Error Cases:** Verify invalid JSON is handled gracefully -### Future Enhancements +## Future Enhancements - Add JSON schema validation using JSON Schema Draft 7 - Add pretty-printing option for debugging - Add JSON compression utilities - Add JSON diff/merge utilities - Add JSON path query helpers + +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + +--- + + +## Why Read This? + +This document provides details about CentralizedJsonFormatting. + + +## End-User Guide + +End-user guidance for CentralizedJsonFormatting. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + +## Architecture & Design + +Architecture and design notes for CentralizedJsonFormatting. diff --git a/docs/Providers/AICall/body-metrics-status.md b/docs/Providers/AICall/body-metrics-status.md index d9e88b177..3b924bf10 100644 --- a/docs/Providers/AICall/body-metrics-status.md +++ b/docs/Providers/AICall/body-metrics-status.md @@ -48,9 +48,10 @@ This page documents the core data types that carry conversation state, performan ### AIMetrics - File: `AIMetrics.cs` -- Fields: `Provider`, `Model`, token counters (input/output), `CompletionTime`, `FinishReason` +- Fields: `Provider`, `Model`, token counters (input/output), `CompletionTime`, `FinishReason`, `EstimatedCost` +- `EstimatedCost` is computed from provider/model pricing and token buckets (prompt, cached, cache-write, output generation, reasoning). Missing or negative prices contribute `0`; estimated tokens are used as a fallback when actual tokens are `0`. - Validation: non-empty `Provider`, `Model`, `FinishReason`; non-negative counters and time -- `Combine(other)` merges providers/models/reason and adds counters/time +- `Combine(other)` merges providers/models/reason and adds counters/time/cost ### AICallStatus @@ -318,5 +319,3 @@ var filter = new MetricsFilter(SessionId: "session-123", EventType: AIMetricsEve - Tools: filter with `AIBody.ToolFilter` to avoid unintended tool loading; validate tool args before execution - JSON schema: ensure `JsonOutputSchema` is trusted/user-controlled (avoid TOCTOU injections) - Context: avoid leaking sensitive data when enabling `ContextFilter` - - diff --git a/docs/Providers/AICall/messages.md b/docs/Providers/AICall/messages.md index ee78a4bcc..77029682b 100644 --- a/docs/Providers/AICall/messages.md +++ b/docs/Providers/AICall/messages.md @@ -221,3 +221,85 @@ return output; - Centralized logic reduces duplication and error-prone manual merges. - Preserves structured data (severity, origin), enabling accurate UI surfacing. - Consistent behavior across providers and tools, with clear single-source aggregation. + +## Message codes (machine-readable) + +- Purpose: allow robust programmatic checks without parsing message text. +- Model: `AIRuntimeMessage` now includes `Code: AIMessageCode`. +- Default: `AIMessageCode.Unknown (0)` to keep existing emits backward compatible. + +### Initial codes + +- Provider/model selection: `ProviderMissing`, `UnknownProvider`, `UnknownModel`, `NoCapableModel`, `CapabilityMismatch`. +- Streaming: `StreamingDisabledProvider`, `StreamingUnsupportedModel`. +- Tools/validation: `ToolValidationError`, `BodyInvalid`, `ReturnInvalid`. +- Network/auth: `NetworkTimeout`, `AuthenticationMissing`, `AuthorizationFailed`, `RateLimited`. +- Batch processing: `BatchItemError`, `BatchItemCanceled`, `BatchItemExpired`. + +### Batch processing messages + +Batch operations (via `IAIBatchProvider`) surface item-level results through a unified `IReadOnlyList` on `AIBatchStatus.Messages`: + +| Code | Severity | Origin | When emitted | +|------|----------|--------|--------------| +| `BatchItemError` | Error | Provider | Item returned an error (invalid request, server error, etc.) | +| `BatchItemCanceled` | Error | Provider | Item was canceled before processing (user/system cancellation) | +| `BatchItemExpired` | Warning | Provider | Item expired before being sent to model (24h batch limit exceeded) | + +Providers emitting batch messages: + +- **Anthropic**: Emits all three codes based on `result.type` (`errored`, `canceled`, `expired`) +- **OpenAI**: Emits `BatchItemError` for non-2xx HTTP responses +- **MistralAI**: Emits `BatchItemError` for non-2xx HTTP responses + +All batch messages flow through `ProcessBatchResults()` in `AIStatefulAsyncComponentBase`, which surfaces them via `AIReturn.AddRuntimeMessage()` → `SurfaceMessagesFromReturn()`, mapping severity to Grasshopper runtime message levels (Error → GH Error, Warning → GH Warning). + +### Emission guidance + +- Prefer setting `Code` when raising messages in validators, providers, and policies. +- Keep `Message` human-readable; `Code` is for logic/tests. +- Backward compatibility: existing calls to `new AIRuntimeMessage(sev, origin, text)` automatically default `Code` to `Unknown`. + +### Example (with codes) + +```csharp +// Streaming validation in request +if (settings != null && settings.EnableStreaming == false) +{ + messages.Add(new AIRuntimeMessage( + AIRuntimeMessageSeverity.Error, + AIRuntimeMessageOrigin.Validation, + AIMessageCode.StreamingDisabledProvider, + $"Streaming requested but provider '{provider}' has streaming disabled in settings.")); +} +``` + +### Provider/model validation (AIRequestCall.IsValid()) + +`AIRequestCall.IsValid()` now emits machine-readable codes for provider/model resolution: + +- `ProviderMissing`, `UnknownProvider` +- `NoCapableModel` (no model supports the required capability) +- `UnknownModel` (requested model not registered for provider) +- `CapabilityMismatch` (requested model known but lacks required capability; may be replaced by fallback) +- Body/endpoint issues are tagged as `BodyInvalid` + +Examples: + +```csharp +// Unknown model +messages.Add(new AIRuntimeMessage( + AIRuntimeMessageSeverity.Warning, + AIRuntimeMessageOrigin.Validation, + AIMessageCode.UnknownModel, + $"Requested model '{requestedModel}' is not registered for provider '{provider}'.")); + +// Capability mismatch (selection replaced) +messages.Add(new AIRuntimeMessage( + AIRuntimeMessageSeverity.Warning, + AIRuntimeMessageOrigin.Validation, + AIMessageCode.CapabilityMismatch, + $"Requested model '{requestedModel}' does not support {capability}; selected '{resolvedModel}' instead.")); +``` + +UI components (e.g., `AIStatefulAsyncComponentBase`) should prefer `Message.Code` for badge/state logic and only fall back to text parsing when `Code == Unknown`. diff --git a/docs/Providers/AIModelCapabilities.md b/docs/Providers/AIModelCapabilities.md index 40f18e19f..e7b9446ec 100644 --- a/docs/Providers/AIModelCapabilities.md +++ b/docs/Providers/AIModelCapabilities.md @@ -10,7 +10,7 @@ This document describes `AIModelCapabilities` and how SmartHopper uses it to dri | --- | --- | | **Source Code** | `src/SmartHopper.Infrastructure/AIModels/AIModelCapabilities.cs` | | **Since Version** | ? | -| **Last Updated** | 2026-06-14 | +| **Last Updated** | 2026-08-21 | | **Documentation Maintainer** | Devin AI | _Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ @@ -67,6 +67,12 @@ _Note: This documentation was written by AI on its own. It may contain some mist - **SupportsPromptCaching** Bool; provider supports prompt caching with this model. +- **SupportsBatch** + `bool?` (nullable). When `true`, the model can be used with the provider's batch API for discounted, asynchronous multi-item processing. When `false` or `null`, batch requests are blocked with a runtime diagnostic. + +- **BatchPricing** + Optional `AIModelPricing` with discounted prompt/completion costs used when the model is executed through a batch provider. + - **CacheKeyStrategy** Optional provider-defined cache key hint/strategy name. @@ -132,6 +138,8 @@ var capabilities = new AIModelCapabilities Aliases = new List { "gpt-4o-mini-2024-07-18" }, SupportsStreaming = true, SupportsPromptCaching = false, + SupportsBatch = true, + BatchPricing = new AIModelPricing { Prompt = 0.000001m, Completion = 0.000002m }, DiscouragedForTools = new List() }; diff --git a/docs/Providers/AIProvider.md b/docs/Providers/AIProvider.md index a4c01205c..72072507a 100644 --- a/docs/Providers/AIProvider.md +++ b/docs/Providers/AIProvider.md @@ -10,7 +10,7 @@ Abstract base class implementing `IAIProvider` with shared initialization, setti | --- | --- | | **Source Code** | `src/SmartHopper.Infrastructure/Providers/AIProvider.cs` | | **Since Version** | ? | -| **Last Updated** | 2026-06-14 | +| **Last Updated** | 2026-08-21 | | **Documentation Maintainer** | Devin AI | _Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ @@ -55,6 +55,8 @@ Offer a template-method style pipeline for providers: register models, load sett - HTTP/API orchestration - `Call(request)` handles PreCall, validation, `CallApi`, metrics, PostCall. - `CallApi` supports GET, POST, DELETE, PATCH, Bearer auth, JSON content. +- Batch API support + - Providers can implement `IAIBatchProvider` to expose multi-item submission, status polling, cancellation, result download, and JSONL parsing. - Tools formatting - `GetFormattedTools(toolFilter)` converts registered AITools to function definitions for provider APIs. - Disabled tools (`AITool.Enabled == false`) are filtered out before the category filter is applied. diff --git a/docs/Providers/AIProviderModels.md b/docs/Providers/AIProviderModels.md index fbf9cc561..a259e4854 100644 --- a/docs/Providers/AIProviderModels.md +++ b/docs/Providers/AIProviderModels.md @@ -55,89 +55,27 @@ Expose model lists, capabilities, and defaults to the provider during initializa - For `RetrieveApiModels()`, prefer simple, resilient parsing (e.g., extract `id` or `name` from `data[]`). Handle exceptions internally and return `[]`. - Example providers with dynamic lists: `OpenAIProviderModels`, `MistralAIProviderModels`, `GeminiProviderModels`. ---- - ## Developer Reference -Each provider typically implements `AIProviderModels` to return either a static list of known models or a dynamic list fetched from the provider's API. +Example usage: -```csharp -public class OpenAIProviderModels : AIProviderModels -{ - public override async Task> RetrieveModels() - { - return new List - { - new AIModelCapabilities - { - Provider = "openai", - Model = "gpt-4o", - Capabilities = AICapability.Text2Text | AICapability.ToolChat, - Default = AICapability.ToolChat, - Verified = true, - Rank = 200, - SupportsStreaming = true, - SupportsPromptCaching = false - }, - new AIModelCapabilities - { - Provider = "openai", - Model = "gpt-4o-mini", - Capabilities = AICapability.Text2Text | AICapability.ToolChat | AICapability.Text2Json, - Default = AICapability.Text2Text, - Verified = true, - Rank = 150, - SupportsStreaming = true, - SupportsPromptCaching = false - } - }; - } - - public override async Task> RetrieveApiModels() - { - try - { - var response = await _httpClient.GetAsync("); - response.EnsureSuccessStatusCode(); - var json = await response.Content.ReadAsStringAsync(); - var data = JObject.Parse(json)["data"]; - return data.Select(t => t["id"].ToString()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(s => s) - .ToList(); - } - catch - { - return new List(); - } - } -} +`csharp +// Placeholder example +``r -``` +`csharp +// Another placeholder example +``r -```csharp -// Registration during provider initialization -public override async Task InitializeProviderAsync() -{ - var models = await Models.RetrieveModels(); - foreach (var model in models) - { - ModelManager.RegisterCapabilities(model); - } - await base.InitializeProviderAsync(); -} - -``` - ---- ## Architecture & Design -`AIProviderModels` separates the concern of "what models exist and what can they do" from "how do we select the best model." This separation lets providers focus on provider-specific knowledge while `ModelManager` owns the selection policy. - -The dual methods (`RetrieveModels` and `RetrieveApiModels`) reflect two different use cases: +Architecture and design notes for AIProviderModels. -- **Capability registration** needs rich metadata and should be resilient; a static fallback is acceptable. -- **UI model pickers** want the freshest list possible and can tolerate an empty result by falling back to the static capability list. +```csharp +// Example code for Developer Reference +``` -By returning empty lists on errors rather than throwing, the system remains usable even when a provider API is temporarily unreachable. +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Providers/Gemini.md b/docs/Providers/Gemini.md index 2de3703c4..c3549ade8 100644 --- a/docs/Providers/Gemini.md +++ b/docs/Providers/Gemini.md @@ -2,39 +2,9 @@ The Google Gemini provider integrates Google's Gemini AI models into SmartHopper, supporting text generation, image generation, structured outputs, tool calling, and batch processing. ---- - -## Metadata - -| Property | Value | -| --- | --- | -| **Source Code** | `src/SmartHopper.Infrastructure/Providers/Gemini/GeminiProvider.cs` | -| **Since Version** | ? | -| **Last Updated** | 2026-06-14 | -| **Documentation Maintainer** | Devin AI | - -_Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ - ---- - -## Why Read This? - -This document explains how to configure and use the Google Gemini provider to access Gemini models within SmartHopper. It covers text generation, image generation, batch processing, structured outputs, and thinking/reasoning configuration. - -**You should read this if you:** +## Features -- Want to use Gemini models (Gemini 2.5, 2.0, or 1.5) inside SmartHopper -- Need to generate images or process batch jobs -- Plan to use structured outputs with JSON schema -- Want to configure thinking levels or extra generation parameters - ---- - -## End-User Guide - -### Features - -- **Models**: Gemini 2.5, Gemini 2.0, and Gemini 1.5 models +- **Models**: Gemini 3.1, Gemini 2.5, Gemini 2.0, and Gemini 1.5 models - **Text Generation**: Full support for text-to-text conversations - **Image Generation**: Generate images using Gemini image models - **Structured Outputs**: JSON schema support with Gemini's JSON Schema subset @@ -43,18 +13,18 @@ This document explains how to configure and use the Google Gemini provider to ac - **Batch Processing**: Asynchronous batch job submission and status polling - **Streaming**: Real-time response streaming via Server-Sent Events (SSE) -### Configuration +## Configuration -#### API Key +### API Key Get your API key from [Google AI Studio](https://ai.google.dev/): -1. Visit +1. Visit https://ai.google.dev/ 2. Click "Get API key" 3. Create a new API key 4. Copy the key into SmartHopper's Google provider settings -#### Settings +### Settings - **API Key**: Your Google AI API key (required) - **Model**: Select from available Gemini models (default: `gemini-2.5-flash`) @@ -62,9 +32,9 @@ Get your API key from [Google AI Studio](https://ai.google.dev/): - **Max Tokens**: Maximum output tokens (default: 2000) - **Temperature**: Controls randomness (0.0–2.0, default: 1.0) -#### Extra Parameters +### Extra Parameters -- **Thinking Level**: Gemini 2.5 Flash (`minimal`/`low`/`medium`/`high`) or Gemini 2.5 Pro (integer budget) +- **Thinking Level**: Gemini 3 (`minimal`/`low`/`medium`/`high`) or Gemini 2.5 (integer budget) - **Batch Priority**: Priority for batch jobs (0 = default) - **Image Aspect Ratio**: For image generation (e.g., `16:9`, `1:1`) - **Image Size**: For image generation (e.g., `1K`, `2K`, `4K`) @@ -73,7 +43,29 @@ Get your API key from [Google AI Studio](https://ai.google.dev/): - **Random Seed**: For deterministic outputs - **Safety Level**: Content safety filter level -### JSON Schema Support +## Model Capabilities + +### Gemini 3.1 (Preview) + +- **gemini-3.1-pro-preview**: Full-featured flagship model +- **gemini-3.1-flash-preview**: Fast, efficient model +- **gemini-3-pro-image-preview**: Image generation with reasoning +- **gemini-3.1-flash-image-preview**: Fast image generation + +### Gemini 2.5 (Stable) + +- **gemini-2.5-pro**: High-quality text generation +- **gemini-2.5-flash**: Recommended default for most tasks +- **gemini-2.5-flash-image**: Image generation +- **gemini-2.5-flash-lite**: Lightweight, fast model + +### Gemini 2.0 & 1.5 (Stable/Deprecated) + +- **gemini-2.0-flash**: Stable, verified model +- **gemini-1.5-pro**: Deprecated but available +- **gemini-1.5-flash**: Deprecated but available + +## JSON Schema Support Gemini supports a subset of JSON Schema for structured outputs: @@ -83,14 +75,14 @@ Gemini supports a subset of JSON Schema for structured outputs: **Note**: Gemini 2.0 models require `propertyOrdering` array for proper structure (auto-injected by the provider). -### Thinking/Reasoning +## Thinking/Reasoning Configure thinking levels for extended reasoning: -- **Gemini 2.5 Flash**: Use `thinking_level` extra parameter with values: `minimal`, `low`, `medium`, `high` +- **Gemini 3**: Use `thinking_level` extra parameter with values: `minimal`, `low`, `medium`, `high` - **Gemini 2.5**: Use `thinking_level` as integer string (e.g., `"8192"` for 8K token budget, `"0"` to disable) -### Image Generation +## Image Generation For image generation models: @@ -99,31 +91,7 @@ For image generation models: 3. Optionally configure `image_aspect_ratio` and `image_size` in extras 4. Images are returned as base64-encoded data in the response -### Authentication - -The provider uses Google's `x-goog-api-key` authentication header. Your API key is automatically applied from the settings. - -### Error Handling - -Common errors: - -- **Invalid API Key**: Verify your API key is correct and has appropriate permissions -- **Rate Limiting**: Implement exponential backoff for retries -- **Model Not Available**: Check that the selected model is available in your region -- **Quota Exceeded**: Check your Google Cloud project quotas - -### References - -- [Google AI API Documentation](https://ai.google.dev/api?hl=en) -- [Gemini 3 Developer Guide](https://ai.google.dev/gemini-api/docs/gemini-3) -- [Image Generation Guide](https://ai.google.dev/gemini-api/docs/image-generation) -- [Batch Processing Guide](https://ai.google.dev/gemini-api/docs/batch-processing) - ---- - -## Developer Reference - -### Batch Processing +## Batch Processing Submit multiple requests as a batch: @@ -145,12 +113,11 @@ foreach (var result in status.Results.Values) { // Process result } - ``` Optional: Set `batch_priority` in extras (0 = default, higher = higher priority). -### Streaming +## Streaming Streaming is enabled by default. Responses are streamed in real-time via SSE: @@ -165,21 +132,61 @@ await foreach (var chunk in provider.StreamAsync(request)) { // Process each chunk as it arrives } - ``` -### Tool Calling +## Authentication + +The provider uses Google's `x-goog-api-key` authentication header. Your API key is automatically applied from the settings. + +## Error Handling + +Common errors: + +- **Invalid API Key**: Verify your API key is correct and has appropriate permissions +- **Rate Limiting**: Implement exponential backoff for retries +- **Model Not Available**: Check that the selected model is available in your region +- **Quota Exceeded**: Check your Google Cloud project quotas + +## References + +- [Google AI API Documentation](https://ai.google.dev/api?hl=en) +- [Gemini 3 Developer Guide](https://ai.google.dev/gemini-api/docs/gemini-3) +- [Image Generation Guide](https://ai.google.dev/gemini-api/docs/image-generation) +- [Batch Processing Guide](https://ai.google.dev/gemini-api/docs/batch-processing) + +## Metadata -Gemini supports function calling via native `tools` and `tool_choice` parameters. The provider translates SmartHopper tool definitions into Gemini's format and handles structured argument parsing. +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach --- -## Architecture & Design -The Gemini provider implements the `IAIProvider` interface and translates SmartHopper's generic request/response model into Google's Gemini API format. Key architectural decisions include: +## Why Read This? + +This document provides details about Gemini. + + +## End-User Guide + +End-user guidance for Gemini. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + +## Architecture & Design -- **JSON Schema Subset Mapping**: The provider filters and maps JSON schemas to Gemini's supported subset, automatically injecting `propertyOrdering` for Gemini 2.0 models to preserve object field order. -- **Thinking Level Translation**: Thinking levels are translated into Gemini-specific parameters. For Gemini 2.5 Flash, string values (`minimal`, `low`, `medium`, `high`) are used. For Gemini 2.5 Pro, integer token budgets are passed directly. -- **Image Output Handling**: Image generation responses are decoded from base64-encoded data and surfaced as `AIInteractionImage` instances with appropriate MIME types. -- **Batch Job Management**: Batch submissions are tracked via job IDs with configurable priority levels. The provider polls state transitions and surfaces results through `AIBatchStatus`. -- **Safety Filtering**: Content safety levels are mapped to Gemini's safety category thresholds, allowing per-request control over filtered content. +Architecture and design notes for Gemini. diff --git a/docs/Providers/OpenRouter.md b/docs/Providers/OpenRouter.md index 82a74a530..2661dcd5f 100644 --- a/docs/Providers/OpenRouter.md +++ b/docs/Providers/OpenRouter.md @@ -1,6 +1,6 @@ # OpenRouter Provider -The OpenRouter provider provides access to multiple AI models through a unified OpenAI-compatible interface, supporting text generation, vision, tool calling, structured outputs, reasoning, and streaming. +The OpenRouter provider provides access to multiple AI models through a unified OpenAI-compatible interface, supporting text generation, vision, tool calling, structured outputs, reasoning, streaming, and batch processing. --- @@ -10,7 +10,7 @@ The OpenRouter provider provides access to multiple AI models through a unified | --- | --- | | **Source Code** | `src/SmartHopper.Providers.OpenRouter/` | | **Since Version** | ? | -| **Last Updated** | 2026-06-14 | +| **Last Updated** | 2026-08-21 | | **Documentation Maintainer** | Devin AI | --- @@ -65,6 +65,14 @@ Get your API key from [OpenRouter](https://openrouter.ai/): | **Sort Strategy** | Provider selection sort — `price`, `throughput`, or `latency` | price | | **Data Collection** | Allow or deny provider data collection | deny | +### Batch Processing + +OpenRouter exposes a subset of models with a `:batch` catalog variant. SmartHopper detects these variants and maps them to the same base model slug, while recording the discounted batch pricing and marking the model as batch-eligible (`SupportsBatch`). + +- When a component requests batch execution, SmartHopper verifies that the selected model has `SupportsBatch = true` before routing the request through `IAIBatchProvider`. +- The batch request body uses the base model slug (`openai/gpt-5.6-luna`), not the `:batch` alias. +- Results are polled from OpenRouter's beta batch endpoints and parsed into per-item `AIBatchStatus` results. + ### Common Questions **Q: Why use OpenRouter instead of connecting directly to OpenAI or Anthropic?** diff --git a/docs/Providers/ProviderManager.md b/docs/Providers/ProviderManager.md index 3b3b6d7c6..19bf63444 100644 --- a/docs/Providers/ProviderManager.md +++ b/docs/Providers/ProviderManager.md @@ -15,7 +15,33 @@ Discovers, verifies, registers, and initializes AI providers at runtime. _Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ ---- +- Discovery + - Scans the app-local directory and `%AppData%/SmartHopper/Providers` for `SmartHopper.Providers.*.dll`. + - Each candidate is loaded into a per-provider `AssemblyLoadContext` (see `ProviderAssemblyLoader`) so private dependencies stay isolated. + - SDK type identity (`IAIProviderFactory`) is validated before activation. Mismatch → `Invalid`. + - SemVer compatibility is checked via `BuiltAgainstSdk`/`MinHostSdk` assembly attributes. +- Cryptographic classification (`ProviderClassifier`) + - `Official` — strong-name token matches host AND/OR Authenticode matches host AND/OR SHA-256 is in the published manifest, with no contradicting signal. + - `OfficialTampered` — one signal says official but another contradicts. Always blocked. + - `Community` — valid managed assembly not tied to SmartHopper. Subject to `AllowCommunityProviders` and `BlockNonOfficialProviders`. + - `Invalid` — load failure, missing factory, SDK type-identity mismatch, version incompatibility. +- Trust settings (`SmartHopperSettings`) + - `AllowCommunityProviders` (default `false`): community providers are blocked unless this is on. + - `BlockNonOfficialProviders` (default `false`): hard override that allows only `Official` providers. + - `ProviderIntegrityCheckMode` continues to govern hash-mismatch behavior for Official providers. + - `TrustedProviderRecords` — structured per-provider trust records (legacy `TrustedProviders` boolean map is migrated automatically). +- Registration & initialization + - Duplicate provider ids: Official > Community > everything else. Tampered/Invalid never win. + - `InitializeProviderAsync()` is wrapped in a 30-second per-provider timeout so a hanging provider can't block discovery. +- Accessors + - `GetProviders(includeUntrusted=false)` + - `GetProvider(name)` (handles "Default" indirection) + - `GetProviderSettings(name)`, `GetProviderAssembly(name)`, `GetProviderIcon(name)` + - `GetDefaultAIProvider()` + - `GetProviderClassification(name)`, `IsProviderCommunity(name)`, `IsProviderUnsigned(name)`, `IsProviderMismatched(name)`, `IsProviderUnknown(name)`, `IsProviderUnavailable(name)`, `GetProviderTrustRecord(name)` +- Settings management + - `UpdateProviderSettings(name, Dictionary)` + - Validates via `IAIProviderSettings.ValidateSettings`, persists via `SmartHopperSettings`, refreshes provider cache. ## Why Read This? @@ -45,7 +71,7 @@ ProviderManager is an internal service that runs when SmartHopper starts. It sca ### Visual Guide - + ### Common Questions @@ -164,8 +190,8 @@ ProviderManager.Instance.UpdateProviderSettings("OpenAI", newSettings); ### Data Flow ```text -Rhino Startup → RefreshProviders() → Scan Directory → Verify Trust → -InitializeProviderAsync() → Register in Registry → Available to Components +Rhino Startup → RefreshProviders() → Scan Directory → Verify Trust → +InitializeProviderAsync() → Register in Registry → Available to Components ``` diff --git a/docs/Providers/ProviderSdk.md b/docs/Providers/ProviderSdk.md new file mode 100644 index 000000000..440a08d72 --- /dev/null +++ b/docs/Providers/ProviderSdk.md @@ -0,0 +1,130 @@ +# SmartHopper Provider SDK + +The Provider SDK (`SmartHopper.ProviderSdk`) is a standalone, MIT-licensed assembly that exposes the contracts, base classes, and DTOs a community AI provider needs to integrate with SmartHopper. Provider authors can build against this SDK on a clean machine without cloning or building the rest of the SmartHopper repo. + +## What the SDK contains + +The SDK is **API-connection only**: + +- **Contracts** — `IAIProvider`, `IAIProviderFactory`, `IAIProviderSettings`, `IAIProviderModels`, `IAIBatchProvider`. +- **Base classes** — `AIProvider`, `AIProvider`, `AIProviderSettings`, `AIProviderModels`, `AIProviderStreamingAdapter`. +- **Request / response DTOs** — `AICall.Core.{Base, Interactions, Requests, Returns}`, `AICall.Metrics`, minimal `AICall.JsonSchemas` types referenced by request bodies, and `AICall.Batch` contracts. +- **Model capabilities** — `AIModels.*`, `AIExtraDescriptor`, `AIModelCapabilityRegistry` singleton. +- **Settings descriptors** — `SettingDescriptor`, secret flags, validation result types. +- **Streaming** — `IStreamingAdapter` and provider-facing delta/result types. +- **Tool DTOs** — structures required to encode/decode tool calls and tool results inside a provider response (no `ToolManager`, no tool registration). +- **Compatibility metadata** — `SmartHopperProviderSdkVersionAttribute`, `BuiltAgainstSdkAttribute`, `MinHostSdkAttribute`, `SmartHopperProviderIdAttribute`. + +## What is NOT in the SDK + +These types stay host-side in `SmartHopper.Infrastructure` and the rest of the application: + +- `ProviderManager`, `ProviderHashVerifier`, `ProviderClassifier`, trust dialogs, signature/hash policy, `SmartHopperSettings` persistence, secret storage. +- `AICall.{Policies, Sessions, Validation, Execution, Batch}`. +- `AITools.*` (`ToolManager`, `IAIToolProvider`). +- Rhino/Eto UI, WebChat, badges. + +## Target frameworks + +- `net7.0` (cross-platform; macOS-friendly) +- `net7.0-windows` (Windows-specific surface) + +## License + +The SDK is licensed under **MIT**. SmartHopper itself remains LGPLv3. The permissive license on the SDK lets closed-source community providers link against it without taking on LGPL obligations on their own assemblies. + +## Naming convention + +Provider assemblies discovered at runtime must match `SmartHopper.Providers.*.dll`. SmartHopper scans: + +1. The app-local directory (next to `SmartHopper.Infrastructure.dll`). +2. `%AppData%/SmartHopper/Providers` (Windows) or the platform equivalent under the user's application data folder. + +App-local providers win on duplicate-id conflicts (see plan §2.2). + +## Minimal provider + +```csharp +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; + +[assembly: SmartHopper.ProviderSdk.Metadata.BuiltAgainstSdk("1.0.0")] +[assembly: SmartHopper.ProviderSdk.Metadata.MinHostSdk("1.0.0")] +[assembly: SmartHopper.ProviderSdk.Metadata.SmartHopperProviderId("my-provider")] + +public sealed class MyProviderFactory : IAIProviderFactory +{ + public IAIProvider CreateProvider() => new MyProvider(); + public IAIProviderSettings CreateProviderSettings() => new MyProviderSettings(); +} +``` + +The host instantiates the factory through an isolated `AssemblyLoadContext` so private dependencies of the provider don't leak into the rest of the SmartHopper process. + +## SemVer & compatibility + +- The SDK uses Semantic Versioning. `MAJOR` is reserved for breaking provider contract changes. +- Providers declare both `BuiltAgainstSdk` and `MinHostSdk` via assembly attributes. +- At load time the host enforces `BuiltAgainstSdk.MAJOR == HostSdk.MAJOR` and `HostSdk >= provider.MinHostSdk`. Mismatches classify the provider as `Invalid` and block it with a clear diagnostic. +- Two host majors cannot coexist in one Rhino process and this is documented as unsupported. + +## Trust model + +Community providers (those not cryptographically attributable to SmartHopper) are only loaded when: + +1. `AllowCommunityProviders = true` in `SmartHopperSettings.json`, and +2. `BlockNonOfficialProviders = false`, and +3. The user accepts the per-provider trust prompt the first time a given community DLL is discovered. + +The classification is purely cryptographic — strong-name token + Authenticode (Windows) + the SmartHopper hash manifest. File names and provider ids cannot reach `Official` without one of those signals. + +Community/unsigned providers surface warning runtime messages on every AI component that uses them. + +## Threat notes + +Trusting a community provider grants it full SmartHopper process privileges. The SDK does not sandbox provider code. Community providers can read other providers' settings only through the `IProviderSettingsStore` abstraction, which scopes access by provider id. + +## Metadata + +- Source Code: See source repository. +- Since Version: 2.0.0 +- Last Updated: 2026-07-21 +- Documentation Maintainer: Marc Roca Musach + +--- + + +## Why Read This? + +This document provides details about ProviderSdk. + + +## End-User Guide + +End-user guidance for ProviderSdk. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r + + +## Architecture & Design + +Architecture and design notes for ProviderSdk. + +```csharp +// Example code for Developer Reference +``` + +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Providers/index.md b/docs/Providers/index.md index cf9c37094..2a525c7e7 100644 --- a/docs/Providers/index.md +++ b/docs/Providers/index.md @@ -54,8 +54,11 @@ Providers implement API-specific logic while conforming to a common contract so ### Security -- Signature verification before load, trusted providers tracked in settings. -- Secrets stored using OS secure mechanisms; no hardcoded keys. +- **Cryptographic classification** — providers are classified as `Official`, `OfficialTampered`, `Community`, or `Invalid` based purely on strong-name token, Authenticode signature (Windows), and SHA-256 manifest. Names and provider ids never affect classification. +- **Trust gates** — community providers are blocked unless `AllowCommunityProviders=true`. The global `BlockNonOfficialProviders=true` switch overrides everything to allow Official providers only. +- **Per-provider trust** — first-time discovery of an allowed community provider triggers a trust prompt. Trust is invalidated automatically if the file's SHA-256 changes. +- **Visible warnings** — every AI component using a community/unsigned/unverified provider receives a runtime warning message in Grasshopper. +- **Secrets** — stored using OS secure mechanisms (DPAPI on Windows, Keychain on macOS); no hardcoded keys. Provider code is scoped to its own keys via `IProviderSettingsStore`. ### Extensibility @@ -65,6 +68,7 @@ Providers implement API-specific logic while conforming to a common contract so ### Detailed docs +- [Provider SDK (community-facing)](./ProviderSdk.md) - [ProviderManager](./ProviderManager.md) - [IAIProvider](./IAIProvider.md) - [AIProvider](./AIProvider.md) @@ -79,76 +83,36 @@ Providers implement API-specific logic while conforming to a common contract so - [Prompt Caching](./PromptCaching.md) - [Streaming Adapters](./AICall/Streaming.md) -### Provider implementations (in order of implementation) +## Provider implementations (in order of implementation) -- [MistralAI](./MistralAI.md) -- [OpenAI](./OpenAI.md) -- [DeepSeek](./DeepSeek.md) -- [Anthropic](./Anthropic.md) -- [OpenRouter](./OpenRouter.md) +- MistralAI +- OpenAI +- DeepSeek +- Anthropic +- OpenRouter - [Google Gemini](./Gemini.md) ---- - ## Developer Reference -The provider system uses a template-method pattern where `AIProvider` defines the orchestration and concrete providers fill in encoding, decoding, and API details. - -```csharp -// Implementing a custom provider -public class MyCustomProvider : AIProvider -{ - public override string Name => "mycustom"; - public override string IconUrl => "Resources/mycustom.png"; - - public override async Task InitializeProviderAsync() - { - // Register models and capabilities - var models = await Models.RetrieveModels(); - foreach (var model in models) - { - ModelManager.RegisterCapabilities(model); - } - await base.InitializeProviderAsync(); - } - - protected override object Encode(AIRequestCall request) - { - // Convert SmartHopper request to provider-specific format - return new { model = request.Model, messages = request.Messages }; - } - - protected override AIReturn Decode(string responseBody) - { - // Parse provider response into SmartHopper format - return new AIReturn { Result = responseBody }; - } -} - -``` +Example usage: -```csharp -// Factory registration for discovery -public class MyCustomProviderFactory : IAIProviderFactory -{ - public IAIProvider CreateProvider() - { - return new MyCustomProvider(); - } -} +`csharp +// Placeholder example +``r -``` +`csharp +// Another placeholder example +``r ---- ## Architecture & Design -The provider architecture is designed around separation of concerns: +Architecture and design notes for index. -- **Contracts (`IAIProvider`)** define what every provider must expose: name, icon, model lists, settings, and the call pipeline. -- **Base class (`AIProvider`)** implements shared concerns: HTTP orchestration, settings caching, model resolution via `ModelManager`, metrics recording, and the PreCall/Call/PostCall lifecycle. -- **Model metadata (`AIProviderModels`)** lets each provider declare what models it supports, their capabilities, and which model is default for each capability. -- **Settings (`AIProviderSettings`)** provide descriptor-driven UI and validation so each provider can expose its own configuration without building custom UI. -- **Discovery (`ProviderManager`)** scans for provider assemblies, verifies signatures, and registers trusted providers so the system remains secure even with third-party extensions. +```csharp +// Example code for Developer Reference +``` -This architecture means adding a new AI service typically requires only: a provider class inheriting `AIProvider`, a models class inheriting `AIProviderModels`, a settings class inheriting `AIProviderSettings`, and a factory implementing `IAIProviderFactory`. +```csharp +// Additional example for Developer Reference +``` \ No newline at end of file diff --git a/docs/Tools/ToolResultEnvelope.md b/docs/Tools/ToolResultEnvelope.md index c44f755c1..a2141f817 100644 --- a/docs/Tools/ToolResultEnvelope.md +++ b/docs/Tools/ToolResultEnvelope.md @@ -15,61 +15,17 @@ A non-breaking, standardized metadata wrapper for AI tool results. It stores mac _Note: This documentation was written by AI on its own. It may contain some mistakes. If you would like to help, read this documentation and delete this comment if everything is okay._ ---- - -## Why Read This? - -`ToolResultEnvelope` solves the problem of attaching rich metadata to AI tool results without breaking existing consumers or altering payload shape. Understanding it is essential when authoring new tools or consuming tool results in downstream components. - -**You should read this if you:** - -- Are implementing a new AI tool and need to attach metadata to its result -- Need to parse or classify tool results consistently across different providers -- Want to understand how SmartHopper preserves backward compatibility when evolving tool output formats - ---- - -## End-User Guide - -### Why it matters - -- **Non-breaking**: Existing payloads remain under their usual keys; envelope adds metadata under a reserved key. -- **Consistent parsing**: Downstream consumers can reliably find payload and classify content type. -- **Traceability**: Tracks tool, provider, model, and tool call identifiers. -- **Human readable**: Allows people to quickly understand what a result is and where to find content. - -### Content types - -Use `ToolResultContentType` to classify payloads: - -- **Text**: single string value under "result" -- **List**: array under "list" or a result that is a list -- **Object**: structured JSON object -- **Image**: image URL/base64 or a bitmap-like object -- **Binary**: opaque bytes or binary-like structure - -### Backward compatibility - -- Existing tool results remain valid; envelope is additive under a reserved root key. -- Consumers not aware of the envelope can ignore `"__envelope"` and still read the payload keys. - ---- - -## Developer Reference - -### Schema (fields in "__envelope") - -- `version`: string. Envelope semver (e.g., "1.0"). -- `tool`: string. Tool name (e.g., "text2text", "text2textlist", "text2img"). -- `provider`: string. Provider name (e.g., "OpenAI", "MistralAI", "DeepSeek", "Gemini"). -- `model`: string. Model used. -- `toolCallId`: string. Correlates to the tool call, when available. -- `contentType`: enum ToolResultContentType. One of: Unknown, Text, List, Object, Image, Binary. -- `payloadPath`: string. Path to the payload relative to the root (defaults to "result"). -- `schemaRef`: string. Optional JSON Schema reference (URL or in-repo path). -- `compat`: object. Optional compatibility keys for downstream logic. -- `createdAt`: string. ISO timestamp. -- `tags`: array of strings. Arbitrary labels to help catalog results. +- version: string. Envelope semver (e.g., "1.0"). +- tool: string. Tool name (e.g., "text_generate", "list_generate", "img_generate"). +- provider: string. Provider name (e.g., "OpenAI", "MistralAI", "DeepSeek", "Gemini"). +- model: string. Model used. +- toolCallId: string. Correlates to the tool call, when available. +- contentType: enum ToolResultContentType. One of: Unknown, Text, List, Object, Image, Binary. +- payloadPath: string. Path to the payload relative to the root (defaults to "result"). +- schemaRef: string. Optional JSON Schema reference (URL or in-repo path). +- compat: object. Optional compatibility keys for downstream logic. +- createdAt: string. ISO timestamp. +- tags: array of strings. Arbitrary labels to help catalog results. See `src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelope.cs` for the authoritative implementation and `src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelopeExtensions.cs` for convenience helpers. @@ -177,3 +133,25 @@ The envelope pattern is a classic non-breaking extension strategy: - **Content typing**: `ToolResultContentType` allows generic consumers to branch behavior without inspecting arbitrary payload shapes. - **Traceability**: Every envelope captures `provider`, `model`, `toolCallId`, and `createdAt`, making it easy to audit and debug which AI invocation produced a given result. - **Additive evolution**: New fields can be added to the envelope schema without invalidating existing consumers, because the envelope is optional and self-describing. + +## Why Read This? + +This document provides details about ToolResultEnvelope. + + +## End-User Guide + +End-user guidance for ToolResultEnvelope. + + +## Developer Reference + +Example usage: + +`csharp +// Placeholder example +``r + +`csharp +// Another placeholder example +``r diff --git a/docs/Tools/img2text.md b/docs/Tools/img2text.md index a1f109278..8f77c05d6 100644 --- a/docs/Tools/img2text.md +++ b/docs/Tools/img2text.md @@ -101,12 +101,7 @@ public class img_to_text : AITool ### Key Types -| Type | Purpose | -| --- | --- | -| `img_to_text` | Tool implementation | -| `AITool` | Base class for all SmartHopper AI tools | -| `AIInteractionImage` | Normalized image representation for providers | -| `ToolResultEnvelope` | Standard metadata wrapper for tool results | +When used after `file2md` with `extractImages: true`, pass each image's `base64Data` and `mimeType`: ### Code Examples @@ -116,78 +111,18 @@ public class img_to_text : AITool // Build the tool request var toolRequest = new AIToolRequest("img2text") { - Parameters = new Dictionary - { - { "imageUrl", " }, - { "prompt", "Describe the architectural style of this building." } - } -}; - -// Execute via the provider -var result = await toolManager.ExecuteToolAsync(toolRequest); -var description = result.Payload["description"].ToString(); - -``` - -**Output**: The AI-generated description string. - -#### Processing a Base64 Image from file2md - -```csharp -// After file2md extracts images from a document -foreach (var image in file2mdResult.Images) -{ - var toolRequest = new AIToolRequest("img2text") - { - Parameters = new Dictionary - { - { "imageBase64", image.Base64Data }, - { "mimeType", image.MimeType }, - { "prompt", "Summarize what this diagram shows." } - } - }; - - var result = await toolManager.ExecuteToolAsync(toolRequest); - descriptions.Add(result.Payload["description"].ToString()); + "imageBase64": "", + "mimeType": "image/jpeg", + "prompt": "Summarize what this diagram shows." } - ``` -### Error Handling - -| Error | Cause | Solution | -| --- | --- | --- | -| Missing image input | Neither `imageUrl` nor `imageBase64` provided | Supply one image source | -| Invalid base64 | Corrupted or improperly encoded image data | Verify base64 string is valid | -| Provider not supported | Selected provider lacks `Image2Text` capability | Switch to OpenAI, Anthropic, or MistralAI | -| URL unreachable | `imageUrl` returns 404 or is blocked | Verify URL is publicly accessible | +**Output**: The AI-generated description string. ---- +- [ToolResultEnvelope.md](./ToolResultEnvelope.md) — envelope metadata convention +- `file2md` `describeImages` parameter calls `img_to_text` automatically for each extracted image — see `describeImages`, `imageMode`, and `imageDescriptionPrompt` parameters on the `file2md` tool +- `AIImgToTextComponent` — standalone Grasshopper component that wraps this tool for direct canvas use ## Architecture & Design -### Design Rationale - -**Problem**: Grasshopper workflows need to analyze visual data, but there is no standard way to send images to AI models across different providers. - -**Approach**: Provide a unified `img2text` tool that normalizes image inputs into `AIInteractionImage` objects. Each provider then formats the image into its native API shape (OpenAI `image_url`, Anthropic `image` block, etc.). - -**Trade-offs**: - -- Unified interface (simple for users) vs provider-specific feature loss (some providers support advanced vision features not exposed) -- Base64 encoding (works everywhere) vs URL references (faster, smaller payloads) - -### System Relationships - -```text -Grasshopper Component → AIToolRequest("img2text") → ToolManager → Provider → AI Model - ↑ - AIInteractionImage (normalized) - -``` - -### Related Documentation - -- [ToolResultEnvelope](./ToolResultEnvelope.md) -- `file2md` tool — document conversion that can extract images for analysis -- `AIImg2TextComponent` — standalone Grasshopper component that wraps this tool for direct canvas use (see Components/AI category) +Architecture and design notes for img2text. diff --git a/docs/Tools/index.md b/docs/Tools/index.md index 463895af8..b8ea94272 100644 --- a/docs/Tools/index.md +++ b/docs/Tools/index.md @@ -34,7 +34,14 @@ SmartHopper's tool system extends Grasshopper components with AI-powered capabil ### What Are Tools? -Tools are discrete operations with JSON-schema-defined inputs that the AI can invoke during a conversation. They are also exposed as standalone utilities in Grasshopper components. For example, `text2text` generates text, `gh_tidy_up` organizes the canvas, and `img2text` analyzes images. +| Tool | Description | +|------|-------------| +| text2text | Generates text based on a prompt with optional instructions | +| text2boolean | Evaluates text against criteria and returns boolean assessments | +| text2textlist | Generates a list of items based on a prompt, count, and type (text/number/integer/boolean) | +| text2json | Generates a JSON object from a prompt, conforming strictly to a provided JSON Schema | +| list_filter | Filters list items based on criteria | +| textlist2boolean | Evaluates list items against criteria and returns boolean results | ### When to Use Them diff --git a/global.json b/global.json new file mode 100644 index 000000000..22dfd864b --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestFeature" + } +} diff --git a/samples/SmartHopper.Providers.Sample/NuGet.config b/samples/SmartHopper.Providers.Sample/NuGet.config new file mode 100644 index 000000000..e78a45f89 --- /dev/null +++ b/samples/SmartHopper.Providers.Sample/NuGet.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/samples/SmartHopper.Providers.Sample/README.md b/samples/SmartHopper.Providers.Sample/README.md new file mode 100644 index 000000000..35e53b011 --- /dev/null +++ b/samples/SmartHopper.Providers.Sample/README.md @@ -0,0 +1,34 @@ +# SmartHopper Provider SDK — Sample scaffolding + +This directory contains a starter csproj that builds a SmartHopper community provider against the `SmartHopper.ProviderSdk` package only — no SmartHopper host references required. + +## Layout + +``` +samples/ +└── SmartHopper.Providers.Sample/ + ├── SmartHopper.Providers.Sample.csproj # references SmartHopper.ProviderSdk + └── README.md # this file +``` + +To turn this into a working provider: + +1. Add a class deriving from `SmartHopper.ProviderSdk.AIProviders.AIProvider` (or `AIProvider` for typed responses). +2. Add an `IAIProviderFactory` implementation that returns instances of your provider and its settings. +3. Add an `IAIProviderSettings` subclass with descriptors for API key, model id, etc. +4. Decorate the assembly with: + ```csharp + [assembly: SmartHopper.ProviderSdk.Metadata.BuiltAgainstSdk("1.0.0")] + [assembly: SmartHopper.ProviderSdk.Metadata.MinHostSdk("1.0.0")] + [assembly: SmartHopper.ProviderSdk.Metadata.SmartHopperProviderId("my-provider")] + ``` +5. Build, copy the resulting `SmartHopper.Providers.MyProvider.dll` into either: + - the SmartHopper app-local directory (next to `SmartHopper.Infrastructure.dll`), or + - `%AppData%/SmartHopper/Providers` on Windows (platform equivalent elsewhere). +6. In SmartHopper settings, enable `AllowCommunityProviders` and accept the per-provider trust prompt. + +For full reference docs, see [docs/Providers/ProviderSdk.md](../../docs/Providers/ProviderSdk.md) and the in-tree providers under `src/SmartHopper.Providers.*` for working examples. + +## License + +Sample code is licensed under MIT, matching the SDK itself. SmartHopper host code remains under LGPLv3. diff --git a/samples/SmartHopper.Providers.Sample/SmartHopper.Providers.Sample.csproj b/samples/SmartHopper.Providers.Sample/SmartHopper.Providers.Sample.csproj new file mode 100644 index 000000000..d86a83d85 --- /dev/null +++ b/samples/SmartHopper.Providers.Sample/SmartHopper.Providers.Sample.csproj @@ -0,0 +1,31 @@ + + + + + net7.0;net7.0-windows + SmartHopper.Providers.Sample + SmartHopper.Providers.Sample + enable + latest + true + true + true + CS1591;NU1701 + + + + + + + diff --git a/src/SmartHopper.Components.Test/AiTools/TestCanvasProtectionComponent.cs b/src/SmartHopper.Components.Test/AiTools/TestCanvasProtectionComponent.cs index db354f18a..6050b5d76 100644 --- a/src/SmartHopper.Components.Test/AiTools/TestCanvasProtectionComponent.cs +++ b/src/SmartHopper.Components.Test/AiTools/TestCanvasProtectionComponent.cs @@ -24,10 +24,10 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Contracts; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Test.AiTools { diff --git a/src/SmartHopper.Components.Test/AiTools/TestGhComponentLockComponent.cs b/src/SmartHopper.Components.Test/AiTools/TestGhComponentLockComponent.cs index f760a86f1..38050a78c 100644 --- a/src/SmartHopper.Components.Test/AiTools/TestGhComponentLockComponent.cs +++ b/src/SmartHopper.Components.Test/AiTools/TestGhComponentLockComponent.cs @@ -46,9 +46,9 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Test.AiTools { diff --git a/src/SmartHopper.Components.Test/AiTools/TestGhComponentPreviewComponent.cs b/src/SmartHopper.Components.Test/AiTools/TestGhComponentPreviewComponent.cs index ca0819a8e..a40066773 100644 --- a/src/SmartHopper.Components.Test/AiTools/TestGhComponentPreviewComponent.cs +++ b/src/SmartHopper.Components.Test/AiTools/TestGhComponentPreviewComponent.cs @@ -46,9 +46,9 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Test.AiTools { diff --git a/src/SmartHopper.Components.Test/AiTools/TestGhGetPutComponent.cs b/src/SmartHopper.Components.Test/AiTools/TestGhGetPutComponent.cs index 8df611e66..190422bb6 100644 --- a/src/SmartHopper.Components.Test/AiTools/TestGhGetPutComponent.cs +++ b/src/SmartHopper.Components.Test/AiTools/TestGhGetPutComponent.cs @@ -22,10 +22,10 @@ using System.Linq; using Grasshopper.Kernel; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Test.AiTools { diff --git a/src/SmartHopper.Components.Test/AiTools/TestGhGroupComponent.cs b/src/SmartHopper.Components.Test/AiTools/TestGhGroupComponent.cs index 666733da6..ca5cdbe02 100644 --- a/src/SmartHopper.Components.Test/AiTools/TestGhGroupComponent.cs +++ b/src/SmartHopper.Components.Test/AiTools/TestGhGroupComponent.cs @@ -39,9 +39,9 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Test.AiTools { diff --git a/src/SmartHopper.Components.Test/AiTools/TestGhMoveComponent.cs b/src/SmartHopper.Components.Test/AiTools/TestGhMoveComponent.cs index f984900bc..3454be6a5 100644 --- a/src/SmartHopper.Components.Test/AiTools/TestGhMoveComponent.cs +++ b/src/SmartHopper.Components.Test/AiTools/TestGhMoveComponent.cs @@ -34,9 +34,9 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Test.AiTools { diff --git a/src/SmartHopper.Components.Test/AiTools/TestSmarthopperReadmeComponent.cs b/src/SmartHopper.Components.Test/AiTools/TestSmarthopperReadmeComponent.cs index fd2352781..4b16a2482 100644 --- a/src/SmartHopper.Components.Test/AiTools/TestSmarthopperReadmeComponent.cs +++ b/src/SmartHopper.Components.Test/AiTools/TestSmarthopperReadmeComponent.cs @@ -31,9 +31,9 @@ using System.Drawing; using Grasshopper.Kernel; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Test.AiTools { diff --git a/src/SmartHopper.Components.Test/Providers/TestAnthropicBatchCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestAnthropicBatchCallComponent.cs index 823b12885..25a0a1be5 100644 --- a/src/SmartHopper.Components.Test/Providers/TestAnthropicBatchCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestAnthropicBatchCallComponent.cs @@ -25,13 +25,14 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestAnthropicCancellationComponent.cs b/src/SmartHopper.Components.Test/Providers/TestAnthropicCancellationComponent.cs index cb68c0945..da615f630 100644 --- a/src/SmartHopper.Components.Test/Providers/TestAnthropicCancellationComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestAnthropicCancellationComponent.cs @@ -23,12 +23,14 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestAnthropicDecodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestAnthropicDecodeComponent.cs index f2af4b55b..4f9281b70 100644 --- a/src/SmartHopper.Components.Test/Providers/TestAnthropicDecodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestAnthropicDecodeComponent.cs @@ -25,9 +25,9 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Providers.Anthropic; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestAnthropicEncodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestAnthropicEncodeComponent.cs index 2d729619f..396f7a983 100644 --- a/src/SmartHopper.Components.Test/Providers/TestAnthropicEncodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestAnthropicEncodeComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestAnthropicStandardCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestAnthropicStandardCallComponent.cs index bcf594a76..a1741ae03 100644 --- a/src/SmartHopper.Components.Test/Providers/TestAnthropicStandardCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestAnthropicStandardCallComponent.cs @@ -25,10 +25,10 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestAnthropicToolsComponent.cs b/src/SmartHopper.Components.Test/Providers/TestAnthropicToolsComponent.cs index d88da1dd4..560033399 100644 --- a/src/SmartHopper.Components.Test/Providers/TestAnthropicToolsComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestAnthropicToolsComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestAnthropicVisionComponent.cs b/src/SmartHopper.Components.Test/Providers/TestAnthropicVisionComponent.cs index 58929ddee..bf46528dd 100644 --- a/src/SmartHopper.Components.Test/Providers/TestAnthropicVisionComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestAnthropicVisionComponent.cs @@ -25,11 +25,12 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Providers.Anthropic; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestDeepSeekBatchCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestDeepSeekBatchCallComponent.cs new file mode 100644 index 000000000..9f86803ab --- /dev/null +++ b/src/SmartHopper.Components.Test/Providers/TestDeepSeekBatchCallComponent.cs @@ -0,0 +1,198 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grasshopper.Kernel; +using Grasshopper.Kernel.Types; +using SmartHopper.Core.ComponentBase; +using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; +namespace SmartHopper.Components.Test.Providers +{ + /// + /// Test component for DeepSeek batch API call. + /// + public class TestDeepSeekBatchCallComponent : AIStatefulAsyncComponentBase + { + public override Guid ComponentGuid => new Guid("9D264C28-E309-47BA-894C-7571AA3CBE3F"); + + public override GH_Exposure Exposure => GH_Exposure.tertiary; + + public TestDeepSeekBatchCallComponent() + : base("Test DeepSeek Batch Call", "TEST-DEEPSEEK-BATCH", "Tests DeepSeek batch API call with service_tier=batch and metrics validation", "SmartHopper", "Test/Providers") + { + this.RunOnlyOnInputChanges = false; + this.SetSelectedProviderName("DeepSeek"); + } + + protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager) + { + } + + protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pManager) + { + pManager.AddBooleanParameter("Call Success", "CS", "Batch API call succeeded", GH_ParamAccess.item); + pManager.AddBooleanParameter("Metrics Valid", "MV", "Metrics structure is valid", GH_ParamAccess.item); + pManager.AddTextParameter("Messages", "M", "Test messages", GH_ParamAccess.list); + } + + protected override AsyncWorkerBase CreateWorker(Action progressReporter) + { + return new Worker(this, this.AddRuntimeMessage); + } + + private sealed class Worker : AsyncWorkerBase + { + private GH_Boolean _callSuccess = new GH_Boolean(false); + private GH_Boolean _metricsValid = new GH_Boolean(false); + private List _messages = new List(); + private readonly TestDeepSeekBatchCallComponent _parent; + + public Worker(TestDeepSeekBatchCallComponent parent, Action addRuntimeMessage) + : base(parent, addRuntimeMessage) + { + this._parent = parent; + } + + public override void GatherInput(IGH_DataAccess DA, out int dataCount) + { + dataCount = 1; + } + + public override async Task DoWorkAsync(CancellationToken token) + { + try + { + bool callSuccess = false; + bool metricsValid = false; + + // Create test AIRequestCall with batch parameters + var call = new AIRequestCall(); + var builder = AIBodyBuilder.FromImmutable(call.Body); + builder.Add(new AIInteractionText + { + Agent = AIAgent.Context, + Content = "Say 'batch test' in two words." + }); + call.Body = builder.Build(); + + // Set batch parameters + call.Parameters = new AIRequestParameters + { + Model = "deepseek-chat", + BatchTier = true + }; + + // Get provider from manager + var providerManager = SmartHopper.Infrastructure.AIProviders.ProviderManager.Instance; + var provider = providerManager.GetProvider("DeepSeek"); + + if (provider == null) + { + this._messages.Add(new GH_String("DeepSeek provider not found")); + this._callSuccess = new GH_Boolean(false); + this._metricsValid = new GH_Boolean(false); + await Task.Yield(); + return; + } + + // Make batch API call + IAIReturn result = null; + try + { + result = await provider.Call(call).ConfigureAwait(false); + + if (result != null && result.Body != null && result.Body.InteractionsCount > 0) + { + callSuccess = true; + var lastInteraction = result.Body.Interactions.LastOrDefault() as AIInteractionText; + var responseText = lastInteraction?.Content ?? "No text response"; + this._messages.Add(new GH_String($"Batch API call successful: {responseText.Substring(0, Math.Min(50, responseText.Length))}...")); + } + else + { + this._messages.Add(new GH_String("Batch API call returned empty result")); + } + } + catch (Exception ex) + { + this._messages.Add(new GH_String($"Batch API call failed: {ex.Message}")); + } + + // Validate metrics + if (result?.Metrics != null) + { + metricsValid = true; + + if (result.Metrics.InputTokens <= 0) + { + metricsValid = false; + this._messages.Add(new GH_String("Input tokens not set or invalid")); + } + + if (result.Metrics.OutputTokens <= 0) + { + metricsValid = false; + this._messages.Add(new GH_String("Output tokens not set or invalid")); + } + + if (metricsValid) + { + this._messages.Add(new GH_String($"Metrics valid - Input: {result.Metrics.InputTokens}, Output: {result.Metrics.OutputTokens}")); + } + } + else + { + this._messages.Add(new GH_String("Metrics not populated")); + } + + this._callSuccess = new GH_Boolean(callSuccess); + this._metricsValid = new GH_Boolean(metricsValid); + } + catch (Exception ex) + { + this._callSuccess = new GH_Boolean(false); + this._metricsValid = new GH_Boolean(false); + this._messages.Add(new GH_String($"Error: {ex.Message}")); + this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, ex.Message); + } + finally + { + await Task.Yield(); + } + } + + public override void SetOutput(IGH_DataAccess DA, out string message) + { + this._parent.SetPersistentOutput("Call Success", this._callSuccess, DA); + this._parent.SetPersistentOutput("Metrics Valid", this._metricsValid, DA); + this._parent.SetPersistentOutput("Messages", this._messages, DA); + message = this._callSuccess.Value && this._metricsValid.Value ? "DeepSeek batch call test passed" : "DeepSeek batch call test failed"; + } + } + } +} \ No newline at end of file diff --git a/src/SmartHopper.Components.Test/Providers/TestDeepSeekCancellationComponent.cs b/src/SmartHopper.Components.Test/Providers/TestDeepSeekCancellationComponent.cs index 9c2b942df..ea0aba58a 100644 --- a/src/SmartHopper.Components.Test/Providers/TestDeepSeekCancellationComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestDeepSeekCancellationComponent.cs @@ -23,11 +23,13 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestDeepSeekDecodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestDeepSeekDecodeComponent.cs index 6de6a0837..58a10f5a4 100644 --- a/src/SmartHopper.Components.Test/Providers/TestDeepSeekDecodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestDeepSeekDecodeComponent.cs @@ -25,9 +25,9 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Providers.DeepSeek; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestDeepSeekEncodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestDeepSeekEncodeComponent.cs index 7eed5b61c..3d7fc66f6 100644 --- a/src/SmartHopper.Components.Test/Providers/TestDeepSeekEncodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestDeepSeekEncodeComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestDeepSeekStandardCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestDeepSeekStandardCallComponent.cs index 676f03b66..ee65a3646 100644 --- a/src/SmartHopper.Components.Test/Providers/TestDeepSeekStandardCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestDeepSeekStandardCallComponent.cs @@ -24,10 +24,10 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestDeepSeekToolsComponent.cs b/src/SmartHopper.Components.Test/Providers/TestDeepSeekToolsComponent.cs index e4f7b150d..455c29f67 100644 --- a/src/SmartHopper.Components.Test/Providers/TestDeepSeekToolsComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestDeepSeekToolsComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestDeepSeekVisionComponent.cs b/src/SmartHopper.Components.Test/Providers/TestDeepSeekVisionComponent.cs new file mode 100644 index 000000000..dc4fc5239 --- /dev/null +++ b/src/SmartHopper.Components.Test/Providers/TestDeepSeekVisionComponent.cs @@ -0,0 +1,166 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grasshopper.Kernel; +using Grasshopper.Kernel.Types; +using SmartHopper.Core.ComponentBase; +using SmartHopper.Providers.DeepSeek; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; +namespace SmartHopper.Components.Test.Providers +{ + /// + /// Test component for DeepSeek vision input handling. + /// + public class TestDeepSeekVisionComponent : AIStatefulAsyncComponentBase + { + public override Guid ComponentGuid => new Guid("43A8D588-79AD-4F81-B92A-2FC8CEA04682"); + + public override GH_Exposure Exposure => GH_Exposure.tertiary; + + public TestDeepSeekVisionComponent() + : base("Test DeepSeek Vision", "TEST-DEEPSEEK-VISION", "Tests DeepSeek vision API call with image input", "SmartHopper", "Test/Providers") + { + this.RunOnlyOnInputChanges = false; + this.SetSelectedProviderName("DeepSeek"); + } + + protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager) + { + } + + protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pManager) + { + pManager.AddBooleanParameter("Success", "S", "Test passed", GH_ParamAccess.item); + pManager.AddTextParameter("Messages", "M", "Test messages", GH_ParamAccess.list); + } + + protected override AsyncWorkerBase CreateWorker(Action progressReporter) + { + return new Worker(this, this.AddRuntimeMessage); + } + + private sealed class Worker : AsyncWorkerBase + { + private GH_Boolean _success = new GH_Boolean(false); + private List _messages = new List(); + private readonly TestDeepSeekVisionComponent _parent; + + public Worker(TestDeepSeekVisionComponent parent, Action addRuntimeMessage) + : base(parent, addRuntimeMessage) + { + this._parent = parent; + } + + public override void GatherInput(IGH_DataAccess DA, out int dataCount) + { + dataCount = 1; + } + + public override async Task DoWorkAsync(CancellationToken token) + { + try + { + // Create test AIRequestCall with image content + var call = new AIRequestCall(); + + // Hardcoded minimal base64 PNG (1x1 transparent pixel) + const string base64Image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwAhgGAWjR9awAAAABJRU5ErkJggg=="; + + var builder = AIBodyBuilder.FromImmutable(call.Body); + builder.Add(new AIInteractionText + { + Agent = AIAgent.Context, + Content = "Analyze this image" + }); + builder.Add(new AIInteractionImage + { + ImageData = base64Image + }); + call.Body = builder.Build(); + + // Encode using DeepSeek provider + var provider = AIProvider.Instance; + var encoded = provider.Encode(call); + + // Verify encoding + if (string.IsNullOrEmpty(encoded)) + { + this._success = new GH_Boolean(false); + this._messages.Add(new GH_String("Encoded message is empty")); + await Task.Yield(); + return; + } + + // Check for image content in encoding + if (!encoded.Contains("\"url\"") && !encoded.Contains("\"image_url\"")) + { + this._success = new GH_Boolean(false); + this._messages.Add(new GH_String("Missing image URL or image_url in encoding")); + await Task.Yield(); + return; + } + + if (!encoded.Contains("image/png") && !encoded.Contains("image/jpeg")) + { + this._success = new GH_Boolean(false); + this._messages.Add(new GH_String("Missing image MIME type in encoding")); + await Task.Yield(); + return; + } + + if (!encoded.Contains("base64")) + { + this._success = new GH_Boolean(false); + this._messages.Add(new GH_String("Missing base64 encoding marker")); + await Task.Yield(); + return; + } + + this._success = new GH_Boolean(true); + this._messages.Add(new GH_String("DeepSeek vision encoding successful")); + this._messages.Add(new GH_String("- Image URL present")); + this._messages.Add(new GH_String("- MIME type correctly set")); + this._messages.Add(new GH_String("- Base64 encoding marker present")); + } + catch (Exception ex) + { + this._success = new GH_Boolean(false); + this._messages.Add(new GH_String($"Error: {ex.Message}")); + this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, ex.Message); + } + + await Task.Yield(); + } + + public override void SetOutput(IGH_DataAccess DA, out string message) + { + this._parent.SetPersistentOutput("Success", this._success, DA); + this._parent.SetPersistentOutput("Messages", this._messages, DA); + message = this._success.Value ? "DeepSeek vision test passed" : "DeepSeek vision test failed"; + } + } + } +} \ No newline at end of file diff --git a/src/SmartHopper.Components.Test/Providers/TestGeminiBatchCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestGeminiBatchCallComponent.cs index e112b2a89..2e6845c60 100644 --- a/src/SmartHopper.Components.Test/Providers/TestGeminiBatchCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestGeminiBatchCallComponent.cs @@ -25,13 +25,14 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestGeminiCancellationComponent.cs b/src/SmartHopper.Components.Test/Providers/TestGeminiCancellationComponent.cs index 43bc93b4f..c10bc1bf3 100644 --- a/src/SmartHopper.Components.Test/Providers/TestGeminiCancellationComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestGeminiCancellationComponent.cs @@ -23,12 +23,14 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestGeminiDecodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestGeminiDecodeComponent.cs index 1a4a82d0f..210b66986 100644 --- a/src/SmartHopper.Components.Test/Providers/TestGeminiDecodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestGeminiDecodeComponent.cs @@ -25,9 +25,9 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Providers.Gemini; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestGeminiEncodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestGeminiEncodeComponent.cs index 07078c9b5..e3a7c2cb7 100644 --- a/src/SmartHopper.Components.Test/Providers/TestGeminiEncodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestGeminiEncodeComponent.cs @@ -25,12 +25,14 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestGeminiFunctionCallingComponent.cs b/src/SmartHopper.Components.Test/Providers/TestGeminiFunctionCallingComponent.cs new file mode 100644 index 000000000..f6f3766c3 --- /dev/null +++ b/src/SmartHopper.Components.Test/Providers/TestGeminiFunctionCallingComponent.cs @@ -0,0 +1,189 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Grasshopper.Kernel; +using Grasshopper.Kernel.Types; +using Newtonsoft.Json.Linq; +using SmartHopper.Core.ComponentBase; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +namespace SmartHopper.Components.Test.Providers +{ + /// + /// Test component for Google Gemini function calling. + /// + public class TestGeminiFunctionCallingComponent : AIStatefulAsyncComponentBase + { + + public override Guid ComponentGuid => new Guid("A12F1236-3B3B-498D-9DB9-DC94FEFB0660"); + + public override GH_Exposure Exposure => GH_Exposure.quinary; + + public TestGeminiFunctionCallingComponent() + : base("Test Gemini Function Calling", "TEST-GEMINI-FUNC", "Tests Gemini function calling and response parsing", "SmartHopper", "Test/Providers") + { + this.RunOnlyOnInputChanges = false; + this.SetSelectedProviderName("Gemini"); + } + + protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager) + { + } + + protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pManager) + { + pManager.AddBooleanParameter("Encoding Success", "ES", "Function encoding succeeded", GH_ParamAccess.item); + pManager.AddBooleanParameter("Parsing Success", "PS", "Function result parsing succeeded", GH_ParamAccess.item); + pManager.AddTextParameter("Messages", "M", "Test messages", GH_ParamAccess.list); + } + + protected override AsyncWorkerBase CreateWorker(Action progressReporter) + { + return new Worker(this, this.AddRuntimeMessage); + } + + private sealed class Worker : AsyncWorkerBase + { + private GH_Boolean _encodingSuccess = new GH_Boolean(false); + private GH_Boolean _parsingSuccess = new GH_Boolean(false); + private List _messages = new List(); + private readonly TestGeminiFunctionCallingComponent _parent; + + public Worker(TestGeminiFunctionCallingComponent parent, Action addRuntimeMessage) + : base(parent, addRuntimeMessage) + { + this._parent = parent; + } + + public override void GatherInput(IGH_DataAccess DA, out int dataCount) + { + dataCount = 1; + } + + public override async Task DoWorkAsync(CancellationToken token) + { + try + { + bool encodingSuccess = false; + bool parsingSuccess = false; + + // Create test AIRequestCall with function definitions using AIBodyBuilder + var bodyBuilder = AIBodyBuilder.Create(); + + bodyBuilder.Add(new AIInteractionText + { + Agent = AIAgent.System, + Content = "You have access to functions." + }); + + // Add function call + bodyBuilder.Add(new AIInteractionToolCall + { + Id = "call_sum_123", + Name = "calculate_sum", + Arguments = JObject.Parse("{\"numbers\": [1, 2, 3]}") + }); + + // Add function result + bodyBuilder.Add(new AIInteractionToolResult + { + Result = new JObject { ["content"] = "Sum result: 6" }, + Id = "call_sum_123" + }); + + var call = new AIRequestCall(); + call.Body = bodyBuilder.Build(); + + // Encode using provider from parent component + var provider = this._parent.GetActualAIProvider(); + var encoded = provider.Encode(call); + + // Verify function encoding + if (string.IsNullOrEmpty(encoded)) + { + this._messages.Add(new GH_String("Encoded message is empty")); + this._encodingSuccess = new GH_Boolean(false); + this._parsingSuccess = new GH_Boolean(false); + await Task.Yield(); + return; + } + + if (!encoded.Contains("\"function_calls\"") && !encoded.Contains("\"functionCalls\"")) + { + this._messages.Add(new GH_String("Missing function_calls in encoding")); + this._encodingSuccess = new GH_Boolean(false); + this._parsingSuccess = new GH_Boolean(false); + await Task.Yield(); + return; + } + + if (!encoded.Contains("\"calculate_sum\"")) + { + this._messages.Add(new GH_String("Function name not found in encoding")); + this._encodingSuccess = new GH_Boolean(false); + this._parsingSuccess = new GH_Boolean(false); + await Task.Yield(); + return; + } + + encodingSuccess = true; + this._messages.Add(new GH_String("Function encoding successful")); + this._messages.Add(new GH_String("- Function calls present")); + this._messages.Add(new GH_String("- Function name 'calculate_sum' encoded")); + + // Verify parsing would work (basic structure check) + if (encoded.Contains("\"role\":\"model\"") && + encoded.Contains("\"role\":\"function\"")) + { + parsingSuccess = true; + this._messages.Add(new GH_String("Function result parsing structure valid")); + } + else + { + this._messages.Add(new GH_String("Function result parsing structure invalid")); + } + + this._encodingSuccess = new GH_Boolean(encodingSuccess); + this._parsingSuccess = new GH_Boolean(parsingSuccess); + } + catch (Exception ex) + { + this._encodingSuccess = new GH_Boolean(false); + this._parsingSuccess = new GH_Boolean(false); + this._messages.Add(new GH_String($"Error: {ex.Message}")); + this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, ex.Message); + } + + await Task.Yield(); + } + + public override void SetOutput(IGH_DataAccess DA, out string message) + { + this._parent.SetPersistentOutput("Encoding Success", this._encodingSuccess, DA); + this._parent.SetPersistentOutput("Parsing Success", this._parsingSuccess, DA); + this._parent.SetPersistentOutput("Messages", this._messages, DA); + message = this._encodingSuccess.Value && this._parsingSuccess.Value ? "Gemini function calling test passed" : "Gemini function calling test failed"; + } + } + } +} diff --git a/src/SmartHopper.Components.Test/Providers/TestGeminiStandardCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestGeminiStandardCallComponent.cs index 2772fb27b..b88cc7bc5 100644 --- a/src/SmartHopper.Components.Test/Providers/TestGeminiStandardCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestGeminiStandardCallComponent.cs @@ -26,11 +26,12 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestGeminiToolsComponent.cs b/src/SmartHopper.Components.Test/Providers/TestGeminiToolsComponent.cs index cde7c8ddd..7620e3e0f 100644 --- a/src/SmartHopper.Components.Test/Providers/TestGeminiToolsComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestGeminiToolsComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestGeminiVisionComponent.cs b/src/SmartHopper.Components.Test/Providers/TestGeminiVisionComponent.cs index 23aacc0e9..5c5ed11f1 100644 --- a/src/SmartHopper.Components.Test/Providers/TestGeminiVisionComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestGeminiVisionComponent.cs @@ -25,11 +25,12 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Providers.Gemini; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestMistralAIBatchCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestMistralAIBatchCallComponent.cs index 1a970529e..b9746a657 100644 --- a/src/SmartHopper.Components.Test/Providers/TestMistralAIBatchCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestMistralAIBatchCallComponent.cs @@ -25,13 +25,14 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestMistralAICancellationComponent.cs b/src/SmartHopper.Components.Test/Providers/TestMistralAICancellationComponent.cs index 96901e7b7..eaed1aceb 100644 --- a/src/SmartHopper.Components.Test/Providers/TestMistralAICancellationComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestMistralAICancellationComponent.cs @@ -23,12 +23,14 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestMistralAIDecodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestMistralAIDecodeComponent.cs index de1f98063..b4a999385 100644 --- a/src/SmartHopper.Components.Test/Providers/TestMistralAIDecodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestMistralAIDecodeComponent.cs @@ -25,9 +25,9 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Providers.MistralAI; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestMistralAIEncodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestMistralAIEncodeComponent.cs index 08e0eb734..c1cb39011 100644 --- a/src/SmartHopper.Components.Test/Providers/TestMistralAIEncodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestMistralAIEncodeComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestMistralAIStandardCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestMistralAIStandardCallComponent.cs index 626cdd4f9..c731cf5ea 100644 --- a/src/SmartHopper.Components.Test/Providers/TestMistralAIStandardCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestMistralAIStandardCallComponent.cs @@ -25,11 +25,12 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestMistralAIToolsComponent.cs b/src/SmartHopper.Components.Test/Providers/TestMistralAIToolsComponent.cs index 953979d83..8cc577845 100644 --- a/src/SmartHopper.Components.Test/Providers/TestMistralAIToolsComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestMistralAIToolsComponent.cs @@ -25,12 +25,14 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestMistralAIVisionComponent.cs b/src/SmartHopper.Components.Test/Providers/TestMistralAIVisionComponent.cs index 51ef6cd1f..dec32d0b2 100644 --- a/src/SmartHopper.Components.Test/Providers/TestMistralAIVisionComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestMistralAIVisionComponent.cs @@ -25,11 +25,12 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Providers.MistralAI; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenAIBatchCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenAIBatchCallComponent.cs index a9f0f9744..3e6412d48 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenAIBatchCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenAIBatchCallComponent.cs @@ -25,13 +25,14 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenAICancellationComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenAICancellationComponent.cs index c8094283b..b671d9f9b 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenAICancellationComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenAICancellationComponent.cs @@ -23,12 +23,14 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenAIDecodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenAIDecodeComponent.cs index a7505193c..1091698b2 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenAIDecodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenAIDecodeComponent.cs @@ -25,9 +25,9 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Providers.OpenAI; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenAIEncodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenAIEncodeComponent.cs index d1afe7625..de116aed7 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenAIEncodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenAIEncodeComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenAIStandardCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenAIStandardCallComponent.cs index 66361f60a..3a73c7a54 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenAIStandardCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenAIStandardCallComponent.cs @@ -25,11 +25,12 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenAIToolsComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenAIToolsComponent.cs index bdc123508..09b5707db 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenAIToolsComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenAIToolsComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenAIVisionComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenAIVisionComponent.cs index 3a17ad7f2..212fcfded 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenAIVisionComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenAIVisionComponent.cs @@ -25,12 +25,14 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Providers.OpenAI; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenRouterBatchCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenRouterBatchCallComponent.cs new file mode 100644 index 000000000..0e3811e6d --- /dev/null +++ b/src/SmartHopper.Components.Test/Providers/TestOpenRouterBatchCallComponent.cs @@ -0,0 +1,196 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grasshopper.Kernel; +using Grasshopper.Kernel.Types; +using SmartHopper.Core.ComponentBase; +using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; +namespace SmartHopper.Components.Test.Providers +{ + /// + /// Test component for OpenRouter batch API call. + /// + public class TestOpenRouterBatchCallComponent : AIStatefulAsyncComponentBase + { + public override Guid ComponentGuid => new Guid("D16711BF-CB36-4E28-A12C-6A81AB8550B4"); + + public override GH_Exposure Exposure => GH_Exposure.octonary; + + public TestOpenRouterBatchCallComponent() + : base("Test OpenRouter Batch Call", "TEST-OPENROUTER-BATCH", "Tests OpenRouter batch API call with service_tier=batch and metrics validation", "SmartHopper", "Test/Providers") + { + this.RunOnlyOnInputChanges = false; + this.SetSelectedProviderName("OpenRouter"); + } + + protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager) + { + } + + protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pManager) + { + pManager.AddBooleanParameter("Call Success", "CS", "Batch API call succeeded", GH_ParamAccess.item); + pManager.AddBooleanParameter("Metrics Valid", "MV", "Metrics structure is valid", GH_ParamAccess.item); + pManager.AddTextParameter("Messages", "M", "Test messages", GH_ParamAccess.list); + } + + protected override AsyncWorkerBase CreateWorker(Action progressReporter) + { + return new Worker(this, this.AddRuntimeMessage); + } + + private sealed class Worker : AsyncWorkerBase + { + private GH_Boolean _callSuccess = new GH_Boolean(false); + private GH_Boolean _metricsValid = new GH_Boolean(false); + private List _messages = new List(); + private readonly TestOpenRouterBatchCallComponent _parent; + + public Worker(TestOpenRouterBatchCallComponent parent, Action addRuntimeMessage) + : base(parent, addRuntimeMessage) + { + this._parent = parent; + } + + public override void GatherInput(IGH_DataAccess DA, out int dataCount) + { + dataCount = 1; + } + + public override async Task DoWorkAsync(CancellationToken token) + { + try + { + bool callSuccess = false; + bool metricsValid = false; + + // Create test AIRequestCall with batch parameters + var call = new AIRequestCall(); + var builder = AIBodyBuilder.FromImmutable(call.Body); + builder.Add(new AIInteractionText + { + Agent = AIAgent.Context, + Content = "Say 'batch test' in two words." + }); + call.Body = builder.Build(); + + // Set batch parameters + call.Parameters = new AIRequestParameters + { + Model = "openrouter/auto", + BatchTier = true + }; + + // Get provider from manager + var providerManager = SmartHopper.Infrastructure.AIProviders.ProviderManager.Instance; + var provider = providerManager.GetProvider("OpenRouter"); + + if (provider == null) + { + this._messages.Add(new GH_String("OpenRouter provider not found")); + this._callSuccess = new GH_Boolean(false); + this._metricsValid = new GH_Boolean(false); + await Task.Yield(); + return; + } + + // Make batch API call + IAIReturn result = null; + try + { + result = await provider.Call(call).ConfigureAwait(false); + + if (result != null && result.Body != null && result.Body.InteractionsCount > 0) + { + callSuccess = true; + var lastInteraction = result.Body.Interactions.LastOrDefault() as AIInteractionText; + var responseText = lastInteraction?.Content ?? "No text response"; + this._messages.Add(new GH_String($"Batch API call successful: {responseText.Substring(0, Math.Min(50, responseText.Length))}...")); + } + else + { + this._messages.Add(new GH_String("Batch API call returned empty result")); + } + } + catch (Exception ex) + { + this._messages.Add(new GH_String($"Batch API call failed: {ex.Message}")); + } + + // Validate metrics + if (result?.Metrics != null) + { + metricsValid = true; + + if (result.Metrics.InputTokens <= 0) + { + metricsValid = false; + this._messages.Add(new GH_String("Input tokens not set or invalid")); + } + + if (result.Metrics.OutputTokens <= 0) + { + metricsValid = false; + this._messages.Add(new GH_String("Output tokens not set or invalid")); + } + + if (metricsValid) + { + this._messages.Add(new GH_String($"Metrics valid - Input: {result.Metrics.InputTokens}, Output: {result.Metrics.OutputTokens}")); + } + } + else + { + this._messages.Add(new GH_String("Metrics not populated")); + } + + this._callSuccess = new GH_Boolean(callSuccess); + this._metricsValid = new GH_Boolean(metricsValid); + } + catch (Exception ex) + { + this._callSuccess = new GH_Boolean(false); + this._metricsValid = new GH_Boolean(false); + this._messages.Add(new GH_String($"Error: {ex.Message}")); + this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, ex.Message); + } + + await Task.Yield(); + } + + public override void SetOutput(IGH_DataAccess DA, out string message) + { + this._parent.SetPersistentOutput("Call Success", this._callSuccess, DA); + this._parent.SetPersistentOutput("Metrics Valid", this._metricsValid, DA); + this._parent.SetPersistentOutput("Messages", this._messages, DA); + message = this._callSuccess.Value && this._metricsValid.Value ? "OpenRouter batch call test passed" : "OpenRouter batch call test failed"; + } + } + } +} \ No newline at end of file diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenRouterCancellationComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenRouterCancellationComponent.cs index 8f28b30d8..e311bbadd 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenRouterCancellationComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenRouterCancellationComponent.cs @@ -23,11 +23,13 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenRouterDecodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenRouterDecodeComponent.cs index 55e102fb6..27f36b931 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenRouterDecodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenRouterDecodeComponent.cs @@ -25,9 +25,9 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Providers.OpenRouter; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenRouterEncodeComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenRouterEncodeComponent.cs index dc8aba4a3..8d5125494 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenRouterEncodeComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenRouterEncodeComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenRouterStandardCallComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenRouterStandardCallComponent.cs index aac8a5fb4..73a39c019 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenRouterStandardCallComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenRouterStandardCallComponent.cs @@ -25,10 +25,10 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenRouterToolsComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenRouterToolsComponent.cs index 5235cf5c8..8fd3f0ddf 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenRouterToolsComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenRouterToolsComponent.cs @@ -25,10 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/Providers/TestOpenRouterVisionComponent.cs b/src/SmartHopper.Components.Test/Providers/TestOpenRouterVisionComponent.cs index 068934751..31fe8c437 100644 --- a/src/SmartHopper.Components.Test/Providers/TestOpenRouterVisionComponent.cs +++ b/src/SmartHopper.Components.Test/Providers/TestOpenRouterVisionComponent.cs @@ -25,11 +25,12 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Providers.OpenRouter; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.Test.Providers { diff --git a/src/SmartHopper.Components.Test/SmartHopper.Components.Test.csproj b/src/SmartHopper.Components.Test/SmartHopper.Components.Test.csproj index bf04cb316..b8b05a5cc 100644 --- a/src/SmartHopper.Components.Test/SmartHopper.Components.Test.csproj +++ b/src/SmartHopper.Components.Test/SmartHopper.Components.Test.csproj @@ -56,7 +56,9 @@ + + diff --git a/src/SmartHopper.Components/AI/AIChatComponent.cs b/src/SmartHopper.Components/AI/AIChatComponent.cs index 252b63c7d..7b7808971 100644 --- a/src/SmartHopper.Components/AI/AIChatComponent.cs +++ b/src/SmartHopper.Components/AI/AIChatComponent.cs @@ -31,9 +31,10 @@ using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.UI.Chat; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.AI { diff --git a/src/SmartHopper.Components/AI/AIExtraSettingsComponent.cs b/src/SmartHopper.Components/AI/AIExtraSettingsComponent.cs index 8d8b4e7da..7ed523840 100644 --- a/src/SmartHopper.Components/AI/AIExtraSettingsComponent.cs +++ b/src/SmartHopper.Components/AI/AIExtraSettingsComponent.cs @@ -29,6 +29,7 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.AI { diff --git a/src/SmartHopper.Components/AI/AIFileContextComponent.cs b/src/SmartHopper.Components/AI/AIFileContextComponent.cs new file mode 100644 index 000000000..d487ea9e2 --- /dev/null +++ b/src/SmartHopper.Components/AI/AIFileContextComponent.cs @@ -0,0 +1,96 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Drawing; +using Grasshopper.Kernel; +using SmartHopper.Infrastructure.AIContext; +namespace SmartHopper.Components.AI +{ + /// + /// Component that provides context for AI queries. + /// + public class AIFileContextComponent : GH_Component, IAIContextProvider + { + private string context; + + /// + /// Gets the provider identifier. + /// + public string ProviderId => "file"; + + /// + /// Gets the component's icon. + /// + protected override Bitmap Icon => Properties.Resources.context; + + /// + public override GH_Exposure Exposure => GH_Exposure.tertiary; + + /// + public override IEnumerable Keywords => new[] { + "File Context", + "Context", + "File Description", + "Project Context", + "AI Context", + }; + + /// + /// Initializes a new instance of the class. + /// Constructor for the AI File Context component. + /// + public AIFileContextComponent() + : base("AI File Context", "AIFileCtx", + "Defines the current file context.\n\nFor example, explain the aim of this file, your expectations of the results, the main input parameters, and what to avoid.\n\nAI-powered components will read this information to generate relevant responses.", + "SmartHopper", + "AI") + { + // Register this component as a context provider + AIContextManager.RegisterProvider(this); + } + + protected override void RegisterInputParams(GH_InputParamManager pManager) + { + pManager.AddTextParameter("Context", "C", "Describe which is the aim of this file, which are your expectations of the results, which are the main input parameters, and what to avoid", GH_ParamAccess.item); + } + + protected override void RegisterOutputParams(GH_OutputParamManager pManager) + { + // No outputs needed + } + + protected override void SolveInstance(IGH_DataAccess DA) + { + this.context = string.Empty; + + if (!DA.GetData(0, ref this.context)) + { + return; + } + } + + public Dictionary GetContext() + { + return new Dictionary { { "file-context", this.context } }; + } + + public override Guid ComponentGuid => new ("A7F5D347-9F4E-4A75-B6A9-115C06B6115D"); + } +} diff --git a/src/SmartHopper.Components/AI/AIModelsComponent.cs b/src/SmartHopper.Components/AI/AIModelsComponent.cs index 33e73ac7f..2a9750d97 100644 --- a/src/SmartHopper.Components/AI/AIModelsComponent.cs +++ b/src/SmartHopper.Components/AI/AIModelsComponent.cs @@ -30,10 +30,10 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.ComponentBase.Contracts; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.AI { diff --git a/src/SmartHopper.Components/AI/AISettingsComponent.cs b/src/SmartHopper.Components/AI/AISettingsComponent.cs index 62a9892ae..69b3eb542 100644 --- a/src/SmartHopper.Components/AI/AISettingsComponent.cs +++ b/src/SmartHopper.Components/AI/AISettingsComponent.cs @@ -29,8 +29,9 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Components.AI { diff --git a/src/SmartHopper.Components/Grasshopper/AIGhConnectComponent.cs b/src/SmartHopper.Components/Grasshopper/AIGhConnectComponent.cs index 3af0e9d4c..890b98642 100644 --- a/src/SmartHopper.Components/Grasshopper/AIGhConnectComponent.cs +++ b/src/SmartHopper.Components/Grasshopper/AIGhConnectComponent.cs @@ -29,6 +29,7 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Grasshopper { @@ -168,7 +169,7 @@ public override async Task DoWorkAsync(CancellationToken token) { this.success = false; this.reasoning = "Tool returned no result."; - var errorMsg = toolResult?.Messages?.FirstOrDefault(m => m.Severity == SmartHopper.Infrastructure.Diagnostics.SHRuntimeMessageSeverity.Error); + var errorMsg = toolResult?.Messages?.FirstOrDefault(m => m.Severity == SmartHopper.ProviderSdk.Diagnostics.SHRuntimeMessageSeverity.Error); if (errorMsg != null) { this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, errorMsg.Message); diff --git a/src/SmartHopper.Components/Grasshopper/AIGhReportComponent.cs b/src/SmartHopper.Components/Grasshopper/AIGhReportComponent.cs index 0fb683953..1b151aedb 100644 --- a/src/SmartHopper.Components/Grasshopper/AIGhReportComponent.cs +++ b/src/SmartHopper.Components/Grasshopper/AIGhReportComponent.cs @@ -29,6 +29,7 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Grasshopper { @@ -137,7 +138,7 @@ public override async Task DoWorkAsync(CancellationToken token) if (toolResult == null || !toolResult.Success) { - var errorMsg = toolResult?.Messages?.FirstOrDefault(m => m.Severity == SmartHopper.Infrastructure.Diagnostics.SHRuntimeMessageSeverity.Error); + var errorMsg = toolResult?.Messages?.FirstOrDefault(m => m.Severity == SmartHopper.ProviderSdk.Diagnostics.SHRuntimeMessageSeverity.Error); if (errorMsg != null) { this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, errorMsg.Message); diff --git a/src/SmartHopper.Components/Grasshopper/GhDiffComponents.cs b/src/SmartHopper.Components/Grasshopper/GhDiffComponents.cs index 043ad5fe8..a0827c2f1 100644 --- a/src/SmartHopper.Components/Grasshopper/GhDiffComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhDiffComponents.cs @@ -21,9 +21,9 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/GhGetComponents.cs b/src/SmartHopper.Components/Grasshopper/GhGetComponents.cs index 4feab0b08..1168f5af5 100644 --- a/src/SmartHopper.Components/Grasshopper/GhGetComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhGetComponents.cs @@ -31,11 +31,11 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/GhMergeComponents.cs b/src/SmartHopper.Components/Grasshopper/GhMergeComponents.cs index a0e423369..a7ef378dc 100644 --- a/src/SmartHopper.Components/Grasshopper/GhMergeComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhMergeComponents.cs @@ -21,9 +21,9 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/GhPatchApplyComponents.cs b/src/SmartHopper.Components/Grasshopper/GhPatchApplyComponents.cs index 6ca03afab..7c203229a 100644 --- a/src/SmartHopper.Components/Grasshopper/GhPatchApplyComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhPatchApplyComponents.cs @@ -26,10 +26,10 @@ using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/GhPatchApplyToCanvasComponents.cs b/src/SmartHopper.Components/Grasshopper/GhPatchApplyToCanvasComponents.cs index 925e2e8ae..65418fbe6 100644 --- a/src/SmartHopper.Components/Grasshopper/GhPatchApplyToCanvasComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhPatchApplyToCanvasComponents.cs @@ -28,10 +28,10 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/GhPutComponents.cs b/src/SmartHopper.Components/Grasshopper/GhPutComponents.cs index 178b50e53..aff20f8ea 100644 --- a/src/SmartHopper.Components/Grasshopper/GhPutComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhPutComponents.cs @@ -27,9 +27,9 @@ using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/GhRetrieveComponents.cs b/src/SmartHopper.Components/Grasshopper/GhRetrieveComponents.cs index dfa2494a8..e3b44ba9f 100644 --- a/src/SmartHopper.Components/Grasshopper/GhRetrieveComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhRetrieveComponents.cs @@ -22,9 +22,9 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/GhTidyUpComponents.cs b/src/SmartHopper.Components/Grasshopper/GhTidyUpComponents.cs index bb4c6fb63..2bacc67a9 100644 --- a/src/SmartHopper.Components/Grasshopper/GhTidyUpComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhTidyUpComponents.cs @@ -26,10 +26,10 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/GhValidateComponents.cs b/src/SmartHopper.Components/Grasshopper/GhValidateComponents.cs index 843b03ad8..cf01f4061 100644 --- a/src/SmartHopper.Components/Grasshopper/GhValidateComponents.cs +++ b/src/SmartHopper.Components/Grasshopper/GhValidateComponents.cs @@ -26,7 +26,7 @@ using GhJSON.Core.Validation; using Grasshopper.Kernel; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/OpenGhJSONComponent.cs b/src/SmartHopper.Components/Grasshopper/OpenGhJSONComponent.cs index 0c971f54b..922e089fe 100644 --- a/src/SmartHopper.Components/Grasshopper/OpenGhJSONComponent.cs +++ b/src/SmartHopper.Components/Grasshopper/OpenGhJSONComponent.cs @@ -27,7 +27,7 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/OpenGhPatchComponent.cs b/src/SmartHopper.Components/Grasshopper/OpenGhPatchComponent.cs index ab7c7173d..c4085ee78 100644 --- a/src/SmartHopper.Components/Grasshopper/OpenGhPatchComponent.cs +++ b/src/SmartHopper.Components/Grasshopper/OpenGhPatchComponent.cs @@ -26,7 +26,7 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/SaveGhJSONComponent.cs b/src/SmartHopper.Components/Grasshopper/SaveGhJSONComponent.cs index 0bc03d72c..c72d3cc4e 100644 --- a/src/SmartHopper.Components/Grasshopper/SaveGhJSONComponent.cs +++ b/src/SmartHopper.Components/Grasshopper/SaveGhJSONComponent.cs @@ -23,7 +23,7 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Grasshopper/SaveGhPatchComponent.cs b/src/SmartHopper.Components/Grasshopper/SaveGhPatchComponent.cs index a5658b729..d4b5230de 100644 --- a/src/SmartHopper.Components/Grasshopper/SaveGhPatchComponent.cs +++ b/src/SmartHopper.Components/Grasshopper/SaveGhPatchComponent.cs @@ -23,7 +23,7 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Grasshopper { diff --git a/src/SmartHopper.Components/Img/AIImg2TextComponent.cs b/src/SmartHopper.Components/Img/AIImg2TextComponent.cs index 5e2cbcebe..6f6e121c8 100644 --- a/src/SmartHopper.Components/Img/AIImg2TextComponent.cs +++ b/src/SmartHopper.Components/Img/AIImg2TextComponent.cs @@ -33,11 +33,13 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Img { diff --git a/src/SmartHopper.Components/Input/AIPromptComponent.cs b/src/SmartHopper.Components/Input/AIPromptComponent.cs index a9286c869..cd70f9ddd 100644 --- a/src/SmartHopper.Components/Input/AIPromptComponent.cs +++ b/src/SmartHopper.Components/Input/AIPromptComponent.cs @@ -23,8 +23,8 @@ using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/Audio2AIComponent.cs b/src/SmartHopper.Components/Input/Audio2AIComponent.cs index ba412f19c..abd08def0 100644 --- a/src/SmartHopper.Components/Input/Audio2AIComponent.cs +++ b/src/SmartHopper.Components/Input/Audio2AIComponent.cs @@ -24,7 +24,7 @@ using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/DiscoursePost2AIComponent.cs b/src/SmartHopper.Components/Input/DiscoursePost2AIComponent.cs index d529d51b4..ccb088148 100644 --- a/src/SmartHopper.Components/Input/DiscoursePost2AIComponent.cs +++ b/src/SmartHopper.Components/Input/DiscoursePost2AIComponent.cs @@ -32,11 +32,11 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/DiscourseTopic2AIComponent.cs b/src/SmartHopper.Components/Input/DiscourseTopic2AIComponent.cs index 26f22cb8e..ac12ea4cc 100644 --- a/src/SmartHopper.Components/Input/DiscourseTopic2AIComponent.cs +++ b/src/SmartHopper.Components/Input/DiscourseTopic2AIComponent.cs @@ -32,11 +32,11 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/File2AIComponent.cs b/src/SmartHopper.Components/Input/File2AIComponent.cs index f2f8fd80d..528d20d71 100644 --- a/src/SmartHopper.Components/Input/File2AIComponent.cs +++ b/src/SmartHopper.Components/Input/File2AIComponent.cs @@ -36,11 +36,12 @@ using SmartHopper.Core.Grasshopper.Utils.Internal; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/GhJSON2AIComponent.cs b/src/SmartHopper.Components/Input/GhJSON2AIComponent.cs index fc75e6708..f15c2065f 100644 --- a/src/SmartHopper.Components/Input/GhJSON2AIComponent.cs +++ b/src/SmartHopper.Components/Input/GhJSON2AIComponent.cs @@ -28,10 +28,10 @@ using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/LadybugPost2AIComponent.cs b/src/SmartHopper.Components/Input/LadybugPost2AIComponent.cs index 6f6d35c51..9d2e8e074 100644 --- a/src/SmartHopper.Components/Input/LadybugPost2AIComponent.cs +++ b/src/SmartHopper.Components/Input/LadybugPost2AIComponent.cs @@ -32,11 +32,11 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/LadybugTopic2AIComponent.cs b/src/SmartHopper.Components/Input/LadybugTopic2AIComponent.cs index 3522ce060..78064d35e 100644 --- a/src/SmartHopper.Components/Input/LadybugTopic2AIComponent.cs +++ b/src/SmartHopper.Components/Input/LadybugTopic2AIComponent.cs @@ -32,11 +32,11 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/McNeelPost2AIComponent.cs b/src/SmartHopper.Components/Input/McNeelPost2AIComponent.cs index 3b0024c5c..b64eb29dd 100644 --- a/src/SmartHopper.Components/Input/McNeelPost2AIComponent.cs +++ b/src/SmartHopper.Components/Input/McNeelPost2AIComponent.cs @@ -32,11 +32,11 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/McNeelTopic2AIComponent.cs b/src/SmartHopper.Components/Input/McNeelTopic2AIComponent.cs index a24d97da7..e11478cce 100644 --- a/src/SmartHopper.Components/Input/McNeelTopic2AIComponent.cs +++ b/src/SmartHopper.Components/Input/McNeelTopic2AIComponent.cs @@ -32,11 +32,11 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/Input/Web2AIComponent.cs b/src/SmartHopper.Components/Input/Web2AIComponent.cs index 61bf1e277..403dfa79a 100644 --- a/src/SmartHopper.Components/Input/Web2AIComponent.cs +++ b/src/SmartHopper.Components/Input/Web2AIComponent.cs @@ -37,12 +37,13 @@ using SmartHopper.Core.Grasshopper.Utils.Internal; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Input { diff --git a/src/SmartHopper.Components/JSON/AIText2JsonComponent.cs b/src/SmartHopper.Components/JSON/AIText2JsonComponent.cs index f1c5b100c..6a2559ad8 100644 --- a/src/SmartHopper.Components/JSON/AIText2JsonComponent.cs +++ b/src/SmartHopper.Components/JSON/AIText2JsonComponent.cs @@ -31,12 +31,14 @@ using SmartHopper.Core.ComponentBase; using SmartHopper.Core.ComponentBase.Batch; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Components.JSON { diff --git a/src/SmartHopper.Components/JSON/JsonGetValueComponent.cs b/src/SmartHopper.Components/JSON/JsonGetValueComponent.cs index 04cd9004d..a43cbaa12 100644 --- a/src/SmartHopper.Components/JSON/JsonGetValueComponent.cs +++ b/src/SmartHopper.Components/JSON/JsonGetValueComponent.cs @@ -21,7 +21,7 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Components.JSON { diff --git a/src/SmartHopper.Components/JSON/JsonMergeComponent.cs b/src/SmartHopper.Components/JSON/JsonMergeComponent.cs index a332aae63..94eca8ffe 100644 --- a/src/SmartHopper.Components/JSON/JsonMergeComponent.cs +++ b/src/SmartHopper.Components/JSON/JsonMergeComponent.cs @@ -22,7 +22,7 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Components.JSON { diff --git a/src/SmartHopper.Components/JSON/JsonObjectComponent.cs b/src/SmartHopper.Components/JSON/JsonObjectComponent.cs index e23ba0f65..4520386de 100644 --- a/src/SmartHopper.Components/JSON/JsonObjectComponent.cs +++ b/src/SmartHopper.Components/JSON/JsonObjectComponent.cs @@ -23,7 +23,7 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Components.JSON { diff --git a/src/SmartHopper.Components/JSON/JsonSanitizerComponent.cs b/src/SmartHopper.Components/JSON/JsonSanitizerComponent.cs index d8a6c7302..7044d6309 100644 --- a/src/SmartHopper.Components/JSON/JsonSanitizerComponent.cs +++ b/src/SmartHopper.Components/JSON/JsonSanitizerComponent.cs @@ -24,7 +24,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Components.JSON { diff --git a/src/SmartHopper.Components/JSON/JsonSchemaComponent.cs b/src/SmartHopper.Components/JSON/JsonSchemaComponent.cs index f7b79e3a4..4a0bfbfdd 100644 --- a/src/SmartHopper.Components/JSON/JsonSchemaComponent.cs +++ b/src/SmartHopper.Components/JSON/JsonSchemaComponent.cs @@ -24,7 +24,7 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Components.JSON { diff --git a/src/SmartHopper.Components/JSON/JsonSetValueComponent.cs b/src/SmartHopper.Components/JSON/JsonSetValueComponent.cs index 1478daea7..780ce66d1 100644 --- a/src/SmartHopper.Components/JSON/JsonSetValueComponent.cs +++ b/src/SmartHopper.Components/JSON/JsonSetValueComponent.cs @@ -23,7 +23,7 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Components.JSON { diff --git a/src/SmartHopper.Components/Knowledge/AIFile2MdComponent.cs b/src/SmartHopper.Components/Knowledge/AIFile2MdComponent.cs index 81a731c93..c15aa1f9d 100644 --- a/src/SmartHopper.Components/Knowledge/AIFile2MdComponent.cs +++ b/src/SmartHopper.Components/Knowledge/AIFile2MdComponent.cs @@ -35,12 +35,14 @@ using SmartHopper.Core.Grasshopper.AITools; using SmartHopper.Core.Grasshopper.Utils.Internal; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Utilities; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/AIWeb2MdComponent.cs b/src/SmartHopper.Components/Knowledge/AIWeb2MdComponent.cs index 682b4d72e..2ec018a8c 100644 --- a/src/SmartHopper.Components/Knowledge/AIWeb2MdComponent.cs +++ b/src/SmartHopper.Components/Knowledge/AIWeb2MdComponent.cs @@ -34,11 +34,12 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Grasshopper.AITools; using SmartHopper.Core.Grasshopper.Utils.Internal; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/DiscoursePostGetComponent.cs b/src/SmartHopper.Components/Knowledge/DiscoursePostGetComponent.cs index e6a46ea89..97d04b6e6 100644 --- a/src/SmartHopper.Components/Knowledge/DiscoursePostGetComponent.cs +++ b/src/SmartHopper.Components/Knowledge/DiscoursePostGetComponent.cs @@ -29,11 +29,11 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/DiscoursePostOpenComponent.cs b/src/SmartHopper.Components/Knowledge/DiscoursePostOpenComponent.cs index f25a83574..760e79cff 100644 --- a/src/SmartHopper.Components/Knowledge/DiscoursePostOpenComponent.cs +++ b/src/SmartHopper.Components/Knowledge/DiscoursePostOpenComponent.cs @@ -25,7 +25,7 @@ using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/DiscourseSearchComponent.cs b/src/SmartHopper.Components/Knowledge/DiscourseSearchComponent.cs index 55917c531..9e6e280fe 100644 --- a/src/SmartHopper.Components/Knowledge/DiscourseSearchComponent.cs +++ b/src/SmartHopper.Components/Knowledge/DiscourseSearchComponent.cs @@ -29,11 +29,11 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/File2MdComponent.cs b/src/SmartHopper.Components/Knowledge/File2MdComponent.cs index 9751a224c..db1830886 100644 --- a/src/SmartHopper.Components/Knowledge/File2MdComponent.cs +++ b/src/SmartHopper.Components/Knowledge/File2MdComponent.cs @@ -32,7 +32,7 @@ using SmartHopper.Core.Models; using SmartHopper.Core.Parameters; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/LadybugForumPostGetComponent.cs b/src/SmartHopper.Components/Knowledge/LadybugForumPostGetComponent.cs index 5e9328cc6..0c458c11d 100644 --- a/src/SmartHopper.Components/Knowledge/LadybugForumPostGetComponent.cs +++ b/src/SmartHopper.Components/Knowledge/LadybugForumPostGetComponent.cs @@ -29,11 +29,11 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/LadybugForumPostOpenComponent.cs b/src/SmartHopper.Components/Knowledge/LadybugForumPostOpenComponent.cs index ac909304d..d2fd219a6 100644 --- a/src/SmartHopper.Components/Knowledge/LadybugForumPostOpenComponent.cs +++ b/src/SmartHopper.Components/Knowledge/LadybugForumPostOpenComponent.cs @@ -25,7 +25,7 @@ using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/LadybugForumSearchComponent.cs b/src/SmartHopper.Components/Knowledge/LadybugForumSearchComponent.cs index d15723cb8..e43bb2146 100644 --- a/src/SmartHopper.Components/Knowledge/LadybugForumSearchComponent.cs +++ b/src/SmartHopper.Components/Knowledge/LadybugForumSearchComponent.cs @@ -29,11 +29,11 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/McNeelForumPostGetComponent.cs b/src/SmartHopper.Components/Knowledge/McNeelForumPostGetComponent.cs index d88794dc4..dacaa4d4a 100644 --- a/src/SmartHopper.Components/Knowledge/McNeelForumPostGetComponent.cs +++ b/src/SmartHopper.Components/Knowledge/McNeelForumPostGetComponent.cs @@ -29,11 +29,11 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/McNeelForumPostOpenComponent.cs b/src/SmartHopper.Components/Knowledge/McNeelForumPostOpenComponent.cs index 6625f9b4c..35c7be731 100644 --- a/src/SmartHopper.Components/Knowledge/McNeelForumPostOpenComponent.cs +++ b/src/SmartHopper.Components/Knowledge/McNeelForumPostOpenComponent.cs @@ -25,7 +25,7 @@ using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/McNeelForumSearchComponent.cs b/src/SmartHopper.Components/Knowledge/McNeelForumSearchComponent.cs index a4da66392..648d5780b 100644 --- a/src/SmartHopper.Components/Knowledge/McNeelForumSearchComponent.cs +++ b/src/SmartHopper.Components/Knowledge/McNeelForumSearchComponent.cs @@ -29,11 +29,11 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/Knowledge/Web2MdComponent.cs b/src/SmartHopper.Components/Knowledge/Web2MdComponent.cs index 5e3471bcd..78afe95df 100644 --- a/src/SmartHopper.Components/Knowledge/Web2MdComponent.cs +++ b/src/SmartHopper.Components/Knowledge/Web2MdComponent.cs @@ -29,12 +29,13 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Knowledge { diff --git a/src/SmartHopper.Components/List/AIList2BooleanComponent.cs b/src/SmartHopper.Components/List/AIList2BooleanComponent.cs index 8b9ae3eda..e54270c8f 100644 --- a/src/SmartHopper.Components/List/AIList2BooleanComponent.cs +++ b/src/SmartHopper.Components/List/AIList2BooleanComponent.cs @@ -34,10 +34,11 @@ using SmartHopper.Core.Grasshopper.Converters; using SmartHopper.Core.Grasshopper.Utils; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.List { diff --git a/src/SmartHopper.Components/List/AIListFilter.cs b/src/SmartHopper.Components/List/AIListFilter.cs index ff514bc18..f713e2308 100644 --- a/src/SmartHopper.Components/List/AIListFilter.cs +++ b/src/SmartHopper.Components/List/AIListFilter.cs @@ -31,7 +31,7 @@ using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.List { diff --git a/src/SmartHopper.Components/Misc/CombineMetricsComponent.cs b/src/SmartHopper.Components/Misc/CombineMetricsComponent.cs index b7bf68a03..a4737b505 100644 --- a/src/SmartHopper.Components/Misc/CombineMetricsComponent.cs +++ b/src/SmartHopper.Components/Misc/CombineMetricsComponent.cs @@ -22,7 +22,7 @@ using Grasshopper.Kernel.Data; using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Metrics; namespace SmartHopper.Components.Misc { @@ -183,7 +183,8 @@ private static string SerializeMetric(AIMetrics metrics) new JProperty("tokens_output_generation", metrics.OutputTokensGeneration), new JProperty("finish_reason", metrics.FinishReason), new JProperty("completion_time", metrics.CompletionTime), - new JProperty("context_usage_percent", metrics.ContextUsagePercent)); + new JProperty("context_usage_percent", metrics.ContextUsagePercent), + new JProperty("estimated_cost", metrics.EstimatedCost)); if (metrics.Role != null) { diff --git a/src/SmartHopper.Components/Misc/DeconstructMetricsComponent.cs b/src/SmartHopper.Components/Misc/DeconstructMetricsComponent.cs index e6fc74e50..6096fb374 100644 --- a/src/SmartHopper.Components/Misc/DeconstructMetricsComponent.cs +++ b/src/SmartHopper.Components/Misc/DeconstructMetricsComponent.cs @@ -62,6 +62,7 @@ protected override void RegisterOutputParams(GH_OutputParamManager pManager) pManager.AddIntegerParameter("Data Count", "DC", "The number of data items that were processed by the component. This may not match the total number of items in your input lists. If the component is configured to process data in batches, this value indicates how many batches (or groups) of results the component needs to process.", GH_ParamAccess.tree); pManager.AddIntegerParameter("Iterations Count", "IC", "The number of times the component ran its calculation. If the component was set to recognize and group identical combinations of input items, it only processed each unique combination once and applied the results to all matching outputs. As a result, the iteration count may be less than the total data count.", GH_ParamAccess.tree); pManager.AddTextParameter("Role", "R", "Role label for each metrics entry (e.g. main, fallback:ImageToText, tool:img2text). Null for the primary call.", GH_ParamAccess.tree); + pManager.AddNumberParameter("Estimated Cost", "C", "Estimated cost in USD based on model pricing and token usage.", GH_ParamAccess.tree); } protected override void SolveInstance(IGH_DataAccess DA) @@ -82,6 +83,7 @@ protected override void SolveInstance(IGH_DataAccess DA) var dataCounts = new GH_Structure(); var iterationsCounts = new GH_Structure(); var roles = new GH_Structure(); + var estimatedCosts = new GH_Structure(); foreach (var path in metricsTree.Paths) { @@ -122,6 +124,9 @@ protected override void SolveInstance(IGH_DataAccess DA) dataCounts.Append(new GH_Integer(metricsObject["data_count"]?.Value() ?? 0), path); iterationsCounts.Append(new GH_Integer(metricsObject["iterations_count"]?.Value() ?? 0), path); roles.Append(new GH_String(metricsObject["role"]?.Value()), path); + + var estimatedCost = metricsObject["estimated_cost"]?.Value() ?? 0.0; + estimatedCosts.Append(new GH_Number(estimatedCost), path); } } catch (Exception ex) @@ -141,6 +146,7 @@ protected override void SolveInstance(IGH_DataAccess DA) DA.SetDataTree(7, dataCounts); DA.SetDataTree(8, iterationsCounts); DA.SetDataTree(9, roles); + DA.SetDataTree(10, estimatedCosts); } } } diff --git a/src/SmartHopper.Components/Output/AI2BooleanComponent.cs b/src/SmartHopper.Components/Output/AI2BooleanComponent.cs index 08e9c42f9..c8ce02a81 100644 --- a/src/SmartHopper.Components/Output/AI2BooleanComponent.cs +++ b/src/SmartHopper.Components/Output/AI2BooleanComponent.cs @@ -25,7 +25,7 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2BooleanListComponent.cs b/src/SmartHopper.Components/Output/AI2BooleanListComponent.cs index 182f4aa9a..b434ffba9 100644 --- a/src/SmartHopper.Components/Output/AI2BooleanListComponent.cs +++ b/src/SmartHopper.Components/Output/AI2BooleanListComponent.cs @@ -26,8 +26,8 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2GhJsonComponent.cs b/src/SmartHopper.Components/Output/AI2GhJsonComponent.cs index 9e8771b1e..45f79e0fd 100644 --- a/src/SmartHopper.Components/Output/AI2GhJsonComponent.cs +++ b/src/SmartHopper.Components/Output/AI2GhJsonComponent.cs @@ -25,9 +25,10 @@ using Newtonsoft.Json.Linq; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2ImgComponent.cs b/src/SmartHopper.Components/Output/AI2ImgComponent.cs index 208fa336f..2c38068ca 100644 --- a/src/SmartHopper.Components/Output/AI2ImgComponent.cs +++ b/src/SmartHopper.Components/Output/AI2ImgComponent.cs @@ -26,7 +26,7 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2IntegerComponent.cs b/src/SmartHopper.Components/Output/AI2IntegerComponent.cs index 69619664c..baf891231 100644 --- a/src/SmartHopper.Components/Output/AI2IntegerComponent.cs +++ b/src/SmartHopper.Components/Output/AI2IntegerComponent.cs @@ -25,7 +25,7 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2IntegerListComponent.cs b/src/SmartHopper.Components/Output/AI2IntegerListComponent.cs index 10da448ea..e3a8347ca 100644 --- a/src/SmartHopper.Components/Output/AI2IntegerListComponent.cs +++ b/src/SmartHopper.Components/Output/AI2IntegerListComponent.cs @@ -26,8 +26,8 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2JsonComponent.cs b/src/SmartHopper.Components/Output/AI2JsonComponent.cs index aa13ff4ce..2c3dd0ca1 100644 --- a/src/SmartHopper.Components/Output/AI2JsonComponent.cs +++ b/src/SmartHopper.Components/Output/AI2JsonComponent.cs @@ -28,8 +28,9 @@ using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Grasshopper.Converters; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2MarkdownComponent.cs b/src/SmartHopper.Components/Output/AI2MarkdownComponent.cs index 25809b0b2..4388438ae 100644 --- a/src/SmartHopper.Components/Output/AI2MarkdownComponent.cs +++ b/src/SmartHopper.Components/Output/AI2MarkdownComponent.cs @@ -24,7 +24,7 @@ using Grasshopper.Kernel.Types; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2NumberComponent.cs b/src/SmartHopper.Components/Output/AI2NumberComponent.cs index dd9cedf43..e49a4ae09 100644 --- a/src/SmartHopper.Components/Output/AI2NumberComponent.cs +++ b/src/SmartHopper.Components/Output/AI2NumberComponent.cs @@ -25,7 +25,7 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2NumberListComponent.cs b/src/SmartHopper.Components/Output/AI2NumberListComponent.cs index bbab98047..84e209e6e 100644 --- a/src/SmartHopper.Components/Output/AI2NumberListComponent.cs +++ b/src/SmartHopper.Components/Output/AI2NumberListComponent.cs @@ -26,8 +26,8 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2ScriptComponent.cs b/src/SmartHopper.Components/Output/AI2ScriptComponent.cs index 625d09b5c..c3c7f31aa 100644 --- a/src/SmartHopper.Components/Output/AI2ScriptComponent.cs +++ b/src/SmartHopper.Components/Output/AI2ScriptComponent.cs @@ -25,8 +25,9 @@ using Grasshopper.Kernel.Types; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2SpeechComponent.cs b/src/SmartHopper.Components/Output/AI2SpeechComponent.cs index 934cb4dc6..01335b762 100644 --- a/src/SmartHopper.Components/Output/AI2SpeechComponent.cs +++ b/src/SmartHopper.Components/Output/AI2SpeechComponent.cs @@ -26,7 +26,7 @@ using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Parameters; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2TextComponent.cs b/src/SmartHopper.Components/Output/AI2TextComponent.cs index 6a3e2fc01..ca7f7390a 100644 --- a/src/SmartHopper.Components/Output/AI2TextComponent.cs +++ b/src/SmartHopper.Components/Output/AI2TextComponent.cs @@ -24,7 +24,7 @@ using Grasshopper.Kernel.Types; using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Output/AI2TextListComponent.cs b/src/SmartHopper.Components/Output/AI2TextListComponent.cs index 88515083c..e08efcdf6 100644 --- a/src/SmartHopper.Components/Output/AI2TextListComponent.cs +++ b/src/SmartHopper.Components/Output/AI2TextListComponent.cs @@ -26,8 +26,8 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Components.Output { diff --git a/src/SmartHopper.Components/Script/AIScriptGenerateComponent.cs b/src/SmartHopper.Components/Script/AIScriptGenerateComponent.cs index d629b503b..c0ee47d99 100644 --- a/src/SmartHopper.Components/Script/AIScriptGenerateComponent.cs +++ b/src/SmartHopper.Components/Script/AIScriptGenerateComponent.cs @@ -32,7 +32,8 @@ using SmartHopper.Core.DataTree; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Script { diff --git a/src/SmartHopper.Components/Script/AIScriptReviewComponent.cs b/src/SmartHopper.Components/Script/AIScriptReviewComponent.cs index 62fbc641f..6f56163b7 100644 --- a/src/SmartHopper.Components/Script/AIScriptReviewComponent.cs +++ b/src/SmartHopper.Components/Script/AIScriptReviewComponent.cs @@ -32,7 +32,8 @@ using SmartHopper.Core.DataTree; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Script { diff --git a/src/SmartHopper.Components/Text/AIText2BooleanComponent.cs b/src/SmartHopper.Components/Text/AIText2BooleanComponent.cs index aa041c759..fe3259b53 100644 --- a/src/SmartHopper.Components/Text/AIText2BooleanComponent.cs +++ b/src/SmartHopper.Components/Text/AIText2BooleanComponent.cs @@ -33,10 +33,11 @@ using SmartHopper.Core.Grasshopper.Converters; using SmartHopper.Core.Grasshopper.Utils; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; using CommonDrawing = System.Drawing; namespace SmartHopper.Components.Text diff --git a/src/SmartHopper.Components/Text/AIText2TextComponent.cs b/src/SmartHopper.Components/Text/AIText2TextComponent.cs index 26f7d54de..922a676e6 100644 --- a/src/SmartHopper.Components/Text/AIText2TextComponent.cs +++ b/src/SmartHopper.Components/Text/AIText2TextComponent.cs @@ -30,11 +30,13 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Text { diff --git a/src/SmartHopper.Components/Text/AIText2TextListComponent.cs b/src/SmartHopper.Components/Text/AIText2TextListComponent.cs index eaee3cc1b..e9783b6f0 100644 --- a/src/SmartHopper.Components/Text/AIText2TextListComponent.cs +++ b/src/SmartHopper.Components/Text/AIText2TextListComponent.cs @@ -30,11 +30,13 @@ using SmartHopper.Components.Properties; using SmartHopper.Core.ComponentBase; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Components.Text { diff --git a/src/SmartHopper.Core.Grasshopper.Tests/Converters/OpenXmlConverterTests.cs b/src/SmartHopper.Core.Grasshopper.Tests/Converters/OpenXmlConverterTests.cs index 8223edbaf..d57b19048 100644 --- a/src/SmartHopper.Core.Grasshopper.Tests/Converters/OpenXmlConverterTests.cs +++ b/src/SmartHopper.Core.Grasshopper.Tests/Converters/OpenXmlConverterTests.cs @@ -26,13 +26,13 @@ namespace SmartHopper.Core.Grasshopper.Tests.Converters using DocumentFormat.OpenXml.Packaging; using SmartHopper.Core.Grasshopper.Converters; using SmartHopper.Core.Grasshopper.Converters.Formats; + using Xunit; using A = DocumentFormat.OpenXml.Drawing; using M = DocumentFormat.OpenXml.Math; using P = DocumentFormat.OpenXml.Presentation; using S = DocumentFormat.OpenXml.Spreadsheet; using V = DocumentFormat.OpenXml.Vml; using W = DocumentFormat.OpenXml.Wordprocessing; - using Xunit; /// /// Tests for DOCX, PPTX, and XLSX converters and the shared . diff --git a/src/SmartHopper.Core.Grasshopper/AITools/DiscourseToolsBase.cs b/src/SmartHopper.Core.Grasshopper/AITools/DiscourseToolsBase.cs index 4722fe8f2..d06aecfcc 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/DiscourseToolsBase.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/DiscourseToolsBase.cs @@ -24,15 +24,16 @@ using System.Text; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/File2MdToolResult.cs b/src/SmartHopper.Core.Grasshopper/AITools/File2MdToolResult.cs index 783ba38b3..a6f7abbf7 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/File2MdToolResult.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/File2MdToolResult.cs @@ -24,10 +24,10 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/_gh_parameter_modifier.cs b/src/SmartHopper.Core.Grasshopper/AITools/_gh_parameter_modifier.cs index e4c9bbe16..687d58dcb 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/_gh_parameter_modifier.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/_gh_parameter_modifier.cs @@ -26,11 +26,12 @@ using Rhino; using SmartHopper.Core.Grasshopper.Utils.Canvas; using SmartHopper.Core.Grasshopper.Utils.Components; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/_rhino_get_geometry_3dm.cs b/src/SmartHopper.Core.Grasshopper/AITools/_rhino_get_geometry_3dm.cs index 773e8c28f..51c831517 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/_rhino_get_geometry_3dm.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/_rhino_get_geometry_3dm.cs @@ -22,10 +22,10 @@ using Newtonsoft.Json.Linq; using Rhino.DocObjects; using SmartHopper.Core.Grasshopper.Utils.Rhino; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/_rhino_read_3dm.cs b/src/SmartHopper.Core.Grasshopper/AITools/_rhino_read_3dm.cs index f2eeadc80..d668687d5 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/_rhino_read_3dm.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/_rhino_read_3dm.cs @@ -23,10 +23,10 @@ using Newtonsoft.Json.Linq; using Rhino.DocObjects; using SmartHopper.Core.Grasshopper.Utils.Rhino; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/_script_parameter_modifier.cs b/src/SmartHopper.Core.Grasshopper/AITools/_script_parameter_modifier.cs index d2965e40b..a22c94e2e 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/_script_parameter_modifier.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/_script_parameter_modifier.cs @@ -26,11 +26,12 @@ using Rhino; using SmartHopper.Core.Grasshopper.Utils.Canvas; using SmartHopper.Core.Grasshopper.Utils.Components; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/button_click.cs b/src/SmartHopper.Core.Grasshopper/AITools/button_click.cs index 792ebe5cc..2f1261837 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/button_click.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/button_click.cs @@ -22,11 +22,11 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/file2md.cs b/src/SmartHopper.Core.Grasshopper/AITools/file2md.cs index c0cacaf2a..c3fb84d32 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/file2md.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/file2md.cs @@ -26,11 +26,11 @@ using SmartHopper.Core.Grasshopper.Converters.Formats; using SmartHopper.Core.Grasshopper.Utils.Internal; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/get_available_models.cs b/src/SmartHopper.Core.Grasshopper/AITools/get_available_models.cs index e7ebc9a5c..133c6ab47 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/get_available_models.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/get_available_models.cs @@ -21,12 +21,14 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/get_available_providers.cs b/src/SmartHopper.Core.Grasshopper/AITools/get_available_providers.cs index 6ff3198af..12dbe7202 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/get_available_providers.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/get_available_providers.cs @@ -21,12 +21,14 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_clear.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_clear.cs index adf015d1a..7849a6ec9 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_clear.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_clear.cs @@ -26,11 +26,11 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_component_lock.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_component_lock.cs index 61f8a49c4..82e5440f0 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_component_lock.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_component_lock.cs @@ -23,11 +23,11 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_component_preview.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_component_preview.cs index b9a1b9e46..4b47bac08 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_component_preview.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_component_preview.cs @@ -23,10 +23,10 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_connect.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_connect.cs index 3fcfe72ad..86fdc32c1 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_connect.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_connect.cs @@ -25,10 +25,10 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_diff.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_diff.cs index 1ce23f17e..5caa73d82 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_diff.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_diff.cs @@ -24,10 +24,10 @@ using GhJSON.Core.DiffOperations; using GhJSON.Core.Serialization; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_disconnect.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_disconnect.cs index c2b229cee..dcfc5675c 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_disconnect.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_disconnect.cs @@ -25,10 +25,10 @@ using Grasshopper.Kernel; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_document_save.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_document_save.cs index 3b23b3ad5..b96d6c19a 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_document_save.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_document_save.cs @@ -22,10 +22,10 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_generate.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_generate.cs index bff2b6d10..fe999d446 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_generate.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_generate.cs @@ -29,14 +29,15 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.GhJsonSpec; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_get.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_get.cs index 467107409..6ef9a4e53 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_get.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_get.cs @@ -30,12 +30,12 @@ using Grasshopper.Kernel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIContext; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_group.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_group.cs index dea60aea9..3e93ff8db 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_group.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_group.cs @@ -26,10 +26,10 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Converters; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_list_categories.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_list_categories.cs index e2dd09c08..70982ef9d 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_list_categories.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_list_categories.cs @@ -22,10 +22,10 @@ using System.Threading.Tasks; using Grasshopper; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_list_components.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_list_components.cs index d00c87894..229840ff4 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_list_components.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_list_components.cs @@ -25,10 +25,10 @@ using Grasshopper.Kernel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_merge.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_merge.cs index eebe99392..d896ef8a5 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_merge.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_merge.cs @@ -23,10 +23,10 @@ using GhJSON.Core; using GhJSON.Core.Serialization; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_move.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_move.cs index ad73c10aa..7d7c9c679 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_move.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_move.cs @@ -24,10 +24,10 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_patch_apply.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_patch_apply.cs index ec9291017..66fce52bb 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_patch_apply.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_patch_apply.cs @@ -25,10 +25,10 @@ using GhJSON.Core.DiffOperations; using GhJSON.Core.Serialization; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_patch_validate.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_patch_validate.cs index 0c57d27dc..1470db8ce 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_patch_validate.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_patch_validate.cs @@ -22,10 +22,10 @@ using System.Threading.Tasks; using GhJSON.Core; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_put.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_put.cs index b92775995..da9d9ec99 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_put.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_put.cs @@ -33,12 +33,12 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Infrastructure.Dialogs; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_remove.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_remove.cs index 5004ffab9..3eb368bca 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_remove.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_remove.cs @@ -22,11 +22,11 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_report.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_report.cs index 34826abbb..e79f58bea 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_report.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_report.cs @@ -34,13 +34,14 @@ using Grasshopper.Kernel; using Grasshopper.Kernel.Special; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_smart_connect.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_smart_connect.cs index 0d45b9658..172f5bb70 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_smart_connect.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_smart_connect.cs @@ -29,15 +29,16 @@ using Newtonsoft.Json.Linq; using Rhino; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/gh_tidy_up.cs b/src/SmartHopper.Core.Grasshopper/AITools/gh_tidy_up.cs index 3c21b731e..9c164841e 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/gh_tidy_up.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/gh_tidy_up.cs @@ -29,10 +29,10 @@ using Grasshopper; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/img2text.cs b/src/SmartHopper.Core.Grasshopper/AITools/img2text.cs index f7f0b0442..94500ce6b 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/img2text.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/img2text.cs @@ -21,13 +21,14 @@ using System.Diagnostics; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/list_filter.cs b/src/SmartHopper.Core.Grasshopper/AITools/list_filter.cs index 9ec14314f..e373b5998 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/list_filter.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/list_filter.cs @@ -24,13 +24,14 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/script_edit.cs b/src/SmartHopper.Core.Grasshopper/AITools/script_edit.cs index fe2518e89..bca6f6dbe 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/script_edit.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/script_edit.cs @@ -29,13 +29,14 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Constants; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/script_generate.cs b/src/SmartHopper.Core.Grasshopper/AITools/script_generate.cs index 0ce27cb45..55a89a2dd 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/script_generate.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/script_generate.cs @@ -30,14 +30,15 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Constants; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/script_review.cs b/src/SmartHopper.Core.Grasshopper/AITools/script_review.cs index 9ad4bff0f..821335f04 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/script_review.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/script_review.cs @@ -30,13 +30,14 @@ using SmartHopper.Core.Grasshopper.Utils.Canvas; using SmartHopper.Core.Grasshopper.Utils.Components; using SmartHopper.Core.Grasshopper.Utils.Constants; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/set_ai_provider_and_model.cs b/src/SmartHopper.Core.Grasshopper/AITools/set_ai_provider_and_model.cs index dc28a66cb..b7b8e8f20 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/set_ai_provider_and_model.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/set_ai_provider_and_model.cs @@ -29,12 +29,14 @@ using SmartHopper.Core.ComponentBase.Contracts; using SmartHopper.Core.ComponentBase.Cores; using SmartHopper.Core.Grasshopper.Utils.Canvas; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_ghjson_reference.cs b/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_ghjson_reference.cs index 1e7aa7054..ed0e264a4 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_ghjson_reference.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_ghjson_reference.cs @@ -21,10 +21,10 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.GhJsonSpec; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_readme.cs b/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_readme.cs index 71fc4955b..423513415 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_readme.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_readme.cs @@ -20,10 +20,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_tool_help.cs b/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_tool_help.cs index 3f3dee622..2209f8e0e 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_tool_help.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_tool_help.cs @@ -21,10 +21,10 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_workflows.cs b/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_workflows.cs index bfa4a5cbb..79f4aacc2 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_workflows.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/smarthopper_workflows.cs @@ -20,10 +20,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/speech_generate.cs b/src/SmartHopper.Core.Grasshopper/AITools/speech_generate.cs index eaa06b259..29c787692 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/speech_generate.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/speech_generate.cs @@ -22,15 +22,17 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/text2boolean.cs b/src/SmartHopper.Core.Grasshopper/AITools/text2boolean.cs index eab7f6e05..e8c23ba6b 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/text2boolean.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/text2boolean.cs @@ -24,13 +24,14 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Converters; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/text2img.cs b/src/SmartHopper.Core.Grasshopper/AITools/text2img.cs index ed99c3e00..258e77c98 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/text2img.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/text2img.cs @@ -21,13 +21,14 @@ using System.Diagnostics; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/text2json.cs b/src/SmartHopper.Core.Grasshopper/AITools/text2json.cs index 52b689b00..b149bca05 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/text2json.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/text2json.cs @@ -23,14 +23,15 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/text2text.cs b/src/SmartHopper.Core.Grasshopper/AITools/text2text.cs index 201006e14..cdfb2d34c 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/text2text.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/text2text.cs @@ -21,13 +21,14 @@ using System.Diagnostics; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/text2textlist.cs b/src/SmartHopper.Core.Grasshopper/AITools/text2textlist.cs index c42f5b583..f13d62972 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/text2textlist.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/text2textlist.cs @@ -24,15 +24,16 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/textlist2boolean.cs b/src/SmartHopper.Core.Grasshopper/AITools/textlist2boolean.cs index 47b35af64..1a2898b54 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/textlist2boolean.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/textlist2boolean.cs @@ -25,13 +25,14 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.Grasshopper.Converters; using SmartHopper.Core.Grasshopper.Utils.Parsing; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/AITools/web2md.cs b/src/SmartHopper.Core.Grasshopper/AITools/web2md.cs index d5204c967..7628452bd 100644 --- a/src/SmartHopper.Core.Grasshopper/AITools/web2md.cs +++ b/src/SmartHopper.Core.Grasshopper/AITools/web2md.cs @@ -28,11 +28,11 @@ using SmartHopper.Core.Grasshopper.Converters.Formats; using SmartHopper.Core.Grasshopper.Utils.Internal; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.Grasshopper.AITools { diff --git a/src/SmartHopper.Core.Grasshopper/Converters/Formats/UrlConverter.cs b/src/SmartHopper.Core.Grasshopper/Converters/Formats/UrlConverter.cs index a822befb8..fc7888d7c 100644 --- a/src/SmartHopper.Core.Grasshopper/Converters/Formats/UrlConverter.cs +++ b/src/SmartHopper.Core.Grasshopper/Converters/Formats/UrlConverter.cs @@ -45,6 +45,7 @@ using SmartHopper.Core.Grasshopper.AITools; using SmartHopper.Core.Grasshopper.Utils.Internal; using SmartHopper.Infrastructure.Utils; +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Core.Grasshopper.Converters.Formats { diff --git a/src/SmartHopper.Core.Grasshopper/SmartHopper.Core.Grasshopper.csproj b/src/SmartHopper.Core.Grasshopper/SmartHopper.Core.Grasshopper.csproj index b7b5fa4fa..7bb30cdc7 100644 --- a/src/SmartHopper.Core.Grasshopper/SmartHopper.Core.Grasshopper.csproj +++ b/src/SmartHopper.Core.Grasshopper/SmartHopper.Core.Grasshopper.csproj @@ -63,10 +63,10 @@ {D4C6D3D5-2B3A-4B6C-8C3D-57B2D0D3C1E1} SmartHopper.Core - - - - + + + + diff --git a/src/SmartHopper.Core.Grasshopper/Utils/Internal/ImageProcessingService.cs b/src/SmartHopper.Core.Grasshopper/Utils/Internal/ImageProcessingService.cs index 2d59cf169..2dfc8954b 100644 --- a/src/SmartHopper.Core.Grasshopper/Utils/Internal/ImageProcessingService.cs +++ b/src/SmartHopper.Core.Grasshopper/Utils/Internal/ImageProcessingService.cs @@ -27,10 +27,10 @@ namespace SmartHopper.Core.Grasshopper.Utils.Internal using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.Types; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Returns; /// /// Shared image processing utilities used by file2md and web2md. diff --git a/src/SmartHopper.Core.Grasshopper/Utils/Internal/MarkdownImageBatchProcessor.cs b/src/SmartHopper.Core.Grasshopper/Utils/Internal/MarkdownImageBatchProcessor.cs index a2d0882f6..c4f210790 100644 --- a/src/SmartHopper.Core.Grasshopper/Utils/Internal/MarkdownImageBatchProcessor.cs +++ b/src/SmartHopper.Core.Grasshopper/Utils/Internal/MarkdownImageBatchProcessor.cs @@ -24,10 +24,11 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.Grasshopper.Utils.Internal { diff --git a/src/SmartHopper.Core.Grasshopper/Utils/Parsing/AIResponseParser.cs b/src/SmartHopper.Core.Grasshopper/Utils/Parsing/AIResponseParser.cs index 276a2b2cc..50d8f8c5b 100644 --- a/src/SmartHopper.Core.Grasshopper/Utils/Parsing/AIResponseParser.cs +++ b/src/SmartHopper.Core.Grasshopper/Utils/Parsing/AIResponseParser.cs @@ -24,7 +24,7 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Core.Grasshopper.Utils.Parsing { diff --git a/src/SmartHopper.Core.Grasshopper/Utils/Parsing/BooleanResultResolver.cs b/src/SmartHopper.Core.Grasshopper/Utils/Parsing/BooleanResultResolver.cs index c53f5eb48..c7a421549 100644 --- a/src/SmartHopper.Core.Grasshopper/Utils/Parsing/BooleanResultResolver.cs +++ b/src/SmartHopper.Core.Grasshopper/Utils/Parsing/BooleanResultResolver.cs @@ -20,8 +20,8 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.Grasshopper.Utils.Parsing { diff --git a/src/SmartHopper.Core.Grasshopper/Utils/Parsing/IntegerResultResolver.cs b/src/SmartHopper.Core.Grasshopper/Utils/Parsing/IntegerResultResolver.cs index a79d0a5fc..b35170245 100644 --- a/src/SmartHopper.Core.Grasshopper/Utils/Parsing/IntegerResultResolver.cs +++ b/src/SmartHopper.Core.Grasshopper/Utils/Parsing/IntegerResultResolver.cs @@ -20,8 +20,8 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.Grasshopper.Utils.Parsing { diff --git a/src/SmartHopper.Core.Grasshopper/Utils/Parsing/NumberResultResolver.cs b/src/SmartHopper.Core.Grasshopper/Utils/Parsing/NumberResultResolver.cs index 448e91aff..a7d625d5b 100644 --- a/src/SmartHopper.Core.Grasshopper/Utils/Parsing/NumberResultResolver.cs +++ b/src/SmartHopper.Core.Grasshopper/Utils/Parsing/NumberResultResolver.cs @@ -20,8 +20,8 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.Grasshopper.Utils.Parsing { diff --git a/src/SmartHopper.Core/ComponentBase/AIInputAdapterBase.cs b/src/SmartHopper.Core/ComponentBase/AIInputAdapterBase.cs index 9ee6d773b..5824055bc 100644 --- a/src/SmartHopper.Core/ComponentBase/AIInputAdapterBase.cs +++ b/src/SmartHopper.Core/ComponentBase/AIInputAdapterBase.cs @@ -21,8 +21,8 @@ using Grasshopper.Kernel; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.ComponentBase { diff --git a/src/SmartHopper.Core/ComponentBase/AIOutputAdapterBase.cs b/src/SmartHopper.Core/ComponentBase/AIOutputAdapterBase.cs index 2f3db5e6c..c1aa31086 100644 --- a/src/SmartHopper.Core/ComponentBase/AIOutputAdapterBase.cs +++ b/src/SmartHopper.Core/ComponentBase/AIOutputAdapterBase.cs @@ -31,12 +31,13 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Validation; using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.ComponentBase { @@ -244,7 +245,7 @@ protected override void RegisterOutputParams(GH_OutputParamManager pManager) /// /// Internal wrapper for CombineIntoPersistedMetrics, accessible from nested worker class. /// - internal void CombineIntoPersistedMetricsInternal(Infrastructure.AICall.Metrics.AIMetrics metrics, string role = null) + internal void CombineIntoPersistedMetricsInternal(ProviderSdk.AICall.Metrics.AIMetrics metrics, string role = null) { this.CombineIntoPersistedMetrics(metrics, role); } @@ -468,7 +469,7 @@ private void ProcessMappingsBatchResults( } var allInteractions = new List(); - var customIdToMetrics = new Dictionary>(); + var customIdToMetrics = new Dictionary>(); foreach (var path in primarySentinelTree.Paths) { @@ -490,7 +491,7 @@ private void ProcessMappingsBatchResults( } allInteractions.AddRange(interactions); - var metricsList = new List(); + var metricsList = new List(); foreach (var inter in interactions) { if (inter.Metrics != null) metricsList.Add(inter.Metrics); @@ -560,7 +561,7 @@ private void ProcessMappingsBatchResults( if (allInteractions.Count > 0) { var allMetrics = customIdToMetrics.Values.SelectMany(v => v).ToList(); - this.PersistedMetricsList = new SmartHopper.Infrastructure.AICall.Metrics.AIMetricsList(); + this.PersistedMetricsList = new SmartHopper.ProviderSdk.AICall.Metrics.AIMetricsList(); foreach (var m in allMetrics) { this.PersistedMetricsList.Add(m, "main"); @@ -569,7 +570,7 @@ private void ProcessMappingsBatchResults( var firstEntry = this.PersistedMetricsList.Entries[0]; var batchReturn = new AIReturn(); - var batchRequest = new SmartHopper.Infrastructure.AICall.Core.Requests.AIRequestCall(); + var batchRequest = new SmartHopper.ProviderSdk.AICall.Core.Requests.AIRequestCall(); batchRequest.Initialize( firstEntry.Provider, firstEntry.Model, diff --git a/src/SmartHopper.Core/ComponentBase/AIProviderComponentBase.cs b/src/SmartHopper.Core/ComponentBase/AIProviderComponentBase.cs index 02c770752..069ae9a3e 100644 --- a/src/SmartHopper.Core/ComponentBase/AIProviderComponentBase.cs +++ b/src/SmartHopper.Core/ComponentBase/AIProviderComponentBase.cs @@ -23,6 +23,7 @@ using SmartHopper.Core.ComponentBase.Contracts; using SmartHopper.Core.ComponentBase.Cores; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.ComponentBase { diff --git a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.AI.cs b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.AI.cs index 3a8eb44c5..263064e62 100644 --- a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.AI.cs +++ b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.AI.cs @@ -27,15 +27,16 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase.Batch; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.ComponentBase { @@ -93,6 +94,21 @@ private T TryQueueBatchRequest(AIRequestCall request, string identifier, Func if (!this.IsBatchRequest()) return default(T); + // Per-request safety: if the resolved model is explicitly flagged as + // not batch-compatible, fall back to synchronous execution. + if (!string.IsNullOrWhiteSpace(request?.Model)) + { + var capabilities = AIModelCapabilityRegistry.Instance.GetCapabilities( + this.GetActualAIProviderName(), + request.Model); + + if (capabilities?.SupportsBatch == false) + { + Debug.WriteLine($"[AIStatefulAsync] Batch disabled for model '{request.Model}' (SupportsBatch=false)"); + return default(T); + } + } + try { var index = this._batchState.Queue?.Count ?? 0; @@ -130,7 +146,6 @@ private void ProcessAIResult(AIReturn result, string origin) var metrics = result.Metrics; metrics.DataCount = 1; // One AI call = one processing unit this.CombineIntoPersistedMetrics(metrics, "main"); - this.AppendMetricToTree(metrics); } } diff --git a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Batch.cs b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Batch.cs index 6eca5af15..9519b39de 100644 --- a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Batch.cs +++ b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Batch.cs @@ -28,12 +28,15 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase.Cores; using SmartHopper.Core.ComponentBase.State; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Core.ComponentBase { @@ -82,6 +85,23 @@ protected bool IsBatchRequest() return false; } + + // Also check if the selected model explicitly does not support batch. + var model = this.GetModel(); + if (!string.IsNullOrWhiteSpace(model)) + { + var capabilities = AIModelCapabilityRegistry.Instance.GetCapabilities(providerName, model); + if (capabilities?.SupportsBatch == false) + { + this.SetPersistentRuntimeMessage( + "batch_model_unsupported", + GH_RuntimeMessageLevel.Remark, + $"Model '{model}' does not support batch processing with provider '{providerName}'. Processing in regular mode.", + false); + + return false; + } + } } return true; diff --git a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Lifecycle.cs b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Lifecycle.cs index 2747eae0d..b9c887d68 100644 --- a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Lifecycle.cs +++ b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Lifecycle.cs @@ -30,9 +30,10 @@ using SmartHopper.Core.ComponentBase.Cores; using SmartHopper.Core.ComponentBase.Mixins; using SmartHopper.Core.DataTree; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.ComponentBase { diff --git a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Main.cs b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Main.cs index c4dd61011..fbce8af0e 100644 --- a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Main.cs +++ b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Main.cs @@ -31,14 +31,15 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase.Contracts; using SmartHopper.Core.ComponentBase.Cores; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.ComponentBase { @@ -222,7 +223,7 @@ public abstract partial class AIStatefulAsyncComponentBase : AIProviderComponent /// Gets or sets the authoritative metrics list for multi-provider support. /// Use to add entries. /// - protected Infrastructure.AICall.Metrics.AIMetricsList PersistedMetricsList + protected ProviderSdk.AICall.Metrics.AIMetricsList PersistedMetricsList { get => this._batchState.PersistedMetricsList; set => this._batchState.PersistedMetricsList = value; diff --git a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Metrics.cs b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Metrics.cs index ae53e5ed9..ce153eeaa 100644 --- a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Metrics.cs +++ b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Metrics.cs @@ -25,11 +25,11 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json.Linq; using SmartHopper.Core.ComponentBase.Contracts; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; namespace SmartHopper.Core.ComponentBase { @@ -139,7 +139,8 @@ private JObject SerializeMetricsEntry(AIMetrics metrics) new JProperty("tokens_output_generation", metrics.OutputTokensGeneration), new JProperty("finish_reason", metrics.FinishReason), new JProperty("completion_time", metrics.CompletionTime), - new JProperty("context_usage_percent", metrics.ContextUsagePercent)); + new JProperty("context_usage_percent", metrics.ContextUsagePercent), + new JProperty("estimated_cost", metrics.EstimatedCost)); if (metrics.Role != null) { @@ -207,7 +208,7 @@ protected virtual Dictionary SentinelTransformOutputs(Dictionar protected void CombineIntoPersistedMetrics(AIMetrics metrics, string role = null) { if (metrics == null) return; - this._batchState.PersistedMetricsList ??= new Infrastructure.AICall.Metrics.AIMetricsList(); + this._batchState.PersistedMetricsList ??= new ProviderSdk.AICall.Metrics.AIMetricsList(); this._batchState.PersistedMetricsList.Add(metrics, role); this.AppendMetricToTree(metrics); } @@ -226,7 +227,7 @@ protected void CombineIntoPersistedMetrics(AIMetrics metrics, string role = null protected void CombineIntoPersistedMetricsAtPath(AIMetrics metrics, GH_Path path, string role = null) { if (metrics == null || path == null) return; - this._batchState.PersistedMetricsList ??= new Infrastructure.AICall.Metrics.AIMetricsList(); + this._batchState.PersistedMetricsList ??= new ProviderSdk.AICall.Metrics.AIMetricsList(); this._batchState.PersistedMetricsList.Add(metrics, role); this.AppendMetricToTree(metrics, path); } diff --git a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Persistence.cs b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Persistence.cs index 59f2ace1c..e793294a7 100644 --- a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Persistence.cs +++ b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Persistence.cs @@ -29,13 +29,14 @@ using SmartHopper.Core.ComponentBase.Batch; using SmartHopper.Core.ComponentBase.Contracts; using SmartHopper.Core.ComponentBase.State; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.ComponentBase { @@ -237,7 +238,7 @@ public override bool Read(GH_IO.Serialization.GH_IReader reader) if (!string.IsNullOrEmpty(batchId) && !string.IsNullOrEmpty(providerName)) { - this._batchState.Submission = new SmartHopper.Infrastructure.AICall.Batch.AIBatchSubmission(batchId, providerName, serializedReq, customIds ?? new List().AsReadOnly()); + this._batchState.Submission = new SmartHopper.ProviderSdk.AICall.Batch.AIBatchSubmission(batchId, providerName, serializedReq, customIds ?? new List().AsReadOnly()); Debug.WriteLine($"[AIStatefulAsync] Read: restored batch state, batchId={batchId}, items={customIds?.Count ?? 0}"); // Restore component to Processing state so sentinel values aren't output diff --git a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Processing.cs b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Processing.cs index e5446166d..f5ffb6db5 100644 --- a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Processing.cs +++ b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.Processing.cs @@ -22,13 +22,14 @@ using Grasshopper.Kernel.Data; using Grasshopper.Kernel.Types; using SmartHopper.Core.ComponentBase.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.ComponentBase { @@ -211,7 +212,7 @@ protected GH_Structure ProcessBatchResults( { // Build per-item metrics via AIMetricsList for multi-provider support var allMetrics = customIdToMetrics.Values.SelectMany(v => v).ToList(); - this.PersistedMetricsList = new Infrastructure.AICall.Metrics.AIMetricsList(); + this.PersistedMetricsList = new ProviderSdk.AICall.Metrics.AIMetricsList(); foreach (var m in allMetrics) { this.PersistedMetricsList.Add(m, "main"); diff --git a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.UI.cs b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.UI.cs index 3576ff516..375690dd3 100644 --- a/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.UI.cs +++ b/src/SmartHopper.Core/ComponentBase/AIStatefulAsyncComponentBase.UI.cs @@ -23,11 +23,13 @@ using SmartHopper.Core.ComponentBase.Attributes; using SmartHopper.Core.ComponentBase.Contracts; using SmartHopper.Core.ComponentBase.State; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Core.ComponentBase { @@ -88,7 +90,7 @@ internal void UpdateBadgeCache() new AIInteractionText { Agent = AIAgent.System, Content = "badge-check" }, }; - var req = new SmartHopper.Infrastructure.AICall.Core.Requests.AIRequestCall(); + var req = new SmartHopper.ProviderSdk.AICall.Core.Requests.AIRequestCall(); req.Initialize(providerName, configuredModel ?? string.Empty, interactions, endpoint: "badge_check", capability: this.RequiredCapability, toolFilter: null); // This triggers provider-scoped selection/fallback based on capability diff --git a/src/SmartHopper.Core/ComponentBase/AsyncWorkerBase.cs b/src/SmartHopper.Core/ComponentBase/AsyncWorkerBase.cs index d5c433c6d..b435603f0 100644 --- a/src/SmartHopper.Core/ComponentBase/AsyncWorkerBase.cs +++ b/src/SmartHopper.Core/ComponentBase/AsyncWorkerBase.cs @@ -21,7 +21,7 @@ using System.Threading; using System.Threading.Tasks; using Grasshopper.Kernel; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.ComponentBase { diff --git a/src/SmartHopper.Core/ComponentBase/Attributes/AIProviderComponentAttributes.cs b/src/SmartHopper.Core/ComponentBase/Attributes/AIProviderComponentAttributes.cs index a354bc1d7..ba6af3a68 100644 --- a/src/SmartHopper.Core/ComponentBase/Attributes/AIProviderComponentAttributes.cs +++ b/src/SmartHopper.Core/ComponentBase/Attributes/AIProviderComponentAttributes.cs @@ -25,6 +25,8 @@ using SmartHopper.Core.ComponentBase.Cores; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; using Timer = System.Timers.Timer; namespace SmartHopper.Core.ComponentBase.Attributes diff --git a/src/SmartHopper.Core/ComponentBase/Contracts/IProviderComponent.cs b/src/SmartHopper.Core/ComponentBase/Contracts/IProviderComponent.cs index c58c83dc3..1b36a1592 100644 --- a/src/SmartHopper.Core/ComponentBase/Contracts/IProviderComponent.cs +++ b/src/SmartHopper.Core/ComponentBase/Contracts/IProviderComponent.cs @@ -17,6 +17,7 @@ */ using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.ComponentBase.Contracts { diff --git a/src/SmartHopper.Core/ComponentBase/Cores/BatchRunState.cs b/src/SmartHopper.Core/ComponentBase/Cores/BatchRunState.cs index 2712738bb..fa1c5b3cc 100644 --- a/src/SmartHopper.Core/ComponentBase/Cores/BatchRunState.cs +++ b/src/SmartHopper.Core/ComponentBase/Cores/BatchRunState.cs @@ -18,9 +18,9 @@ using System; using System.Collections.Generic; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Metrics; namespace SmartHopper.Core.ComponentBase.Cores { diff --git a/src/SmartHopper.Core/ComponentBase/Cores/ProviderSelectionCore.cs b/src/SmartHopper.Core/ComponentBase/Cores/ProviderSelectionCore.cs index d3b74171b..b9bff6c87 100644 --- a/src/SmartHopper.Core/ComponentBase/Cores/ProviderSelectionCore.cs +++ b/src/SmartHopper.Core/ComponentBase/Cores/ProviderSelectionCore.cs @@ -23,6 +23,7 @@ using Grasshopper.Kernel; using SmartHopper.Core.ComponentBase.Contracts; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.ComponentBase.Cores { diff --git a/src/SmartHopper.Core/ComponentBase/Mixins/AIRequestParametersGooParser.cs b/src/SmartHopper.Core/ComponentBase/Mixins/AIRequestParametersGooParser.cs index 41d1fc016..67bc7e119 100644 --- a/src/SmartHopper.Core/ComponentBase/Mixins/AIRequestParametersGooParser.cs +++ b/src/SmartHopper.Core/ComponentBase/Mixins/AIRequestParametersGooParser.cs @@ -17,7 +17,7 @@ */ using Grasshopper.Kernel.Types; -using SmartHopper.Infrastructure.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core; namespace SmartHopper.Core.ComponentBase.Mixins { diff --git a/src/SmartHopper.Core/ComponentBase/ProviderComponentBase.cs b/src/SmartHopper.Core/ComponentBase/ProviderComponentBase.cs index 3b9459e75..2e48fa5e0 100644 --- a/src/SmartHopper.Core/ComponentBase/ProviderComponentBase.cs +++ b/src/SmartHopper.Core/ComponentBase/ProviderComponentBase.cs @@ -23,6 +23,7 @@ using SmartHopper.Core.ComponentBase.Contracts; using SmartHopper.Core.ComponentBase.Cores; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Core.ComponentBase { diff --git a/src/SmartHopper.Core/ComponentBase/StatefulComponentBase.cs b/src/SmartHopper.Core/ComponentBase/StatefulComponentBase.cs index d22a2b08e..349089bf9 100644 --- a/src/SmartHopper.Core/ComponentBase/StatefulComponentBase.cs +++ b/src/SmartHopper.Core/ComponentBase/StatefulComponentBase.cs @@ -49,8 +49,9 @@ using SmartHopper.Core.DataTree; using SmartHopper.Core.Diagnostics; using SmartHopper.Core.IO; -using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Core.ComponentBase { diff --git a/src/SmartHopper.Core/DataTree/DataTreeProcessor.cs b/src/SmartHopper.Core/DataTree/DataTreeProcessor.cs index d76769b8a..f96908265 100644 --- a/src/SmartHopper.Core/DataTree/DataTreeProcessor.cs +++ b/src/SmartHopper.Core/DataTree/DataTreeProcessor.cs @@ -24,8 +24,8 @@ using System.Threading.Tasks; using Grasshopper.Kernel.Data; using Grasshopper.Kernel.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.DataTree { diff --git a/src/SmartHopper.Core/Diagnostics/SHRuntimeMessageExtensions.cs b/src/SmartHopper.Core/Diagnostics/SHRuntimeMessageExtensions.cs index 6352a4255..36fdb4e94 100644 --- a/src/SmartHopper.Core/Diagnostics/SHRuntimeMessageExtensions.cs +++ b/src/SmartHopper.Core/Diagnostics/SHRuntimeMessageExtensions.cs @@ -17,7 +17,7 @@ */ using Grasshopper.Kernel; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.Diagnostics { diff --git a/src/SmartHopper.Core/IO/Codecs/AIInputPayloadCodec.cs b/src/SmartHopper.Core/IO/Codecs/AIInputPayloadCodec.cs index f9ddcec2e..cd6fd594e 100644 --- a/src/SmartHopper.Core/IO/Codecs/AIInputPayloadCodec.cs +++ b/src/SmartHopper.Core/IO/Codecs/AIInputPayloadCodec.cs @@ -24,9 +24,10 @@ using Newtonsoft.Json.Linq; using SmartHopper.Core.Models; using SmartHopper.Core.Types; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.IO.Codecs { @@ -72,7 +73,15 @@ public bool TryDecode(string data, out IGH_Goo goo, out string warning) { var json = JObject.Parse(data); var capability = Enum.TryParse(json.Value("capability") ?? string.Empty, out var cap) ? cap : AICapability.None; - var payloadType = Enum.TryParse(json.Value("payloadType") ?? string.Empty, out var pt) ? pt : AIInputPayloadType.Unknown; + var payloadTypeString = json.Value("payloadType") ?? string.Empty; + + // Legacy payloads used "Speech" as a distinct payload type; it is now classified as Audio. + if (string.Equals(payloadTypeString, "Speech", StringComparison.OrdinalIgnoreCase)) + { + payloadTypeString = "Audio"; + } + + var payloadType = Enum.TryParse(payloadTypeString, out var pt) ? pt : AIInputPayloadType.Unknown; var hint = json.Value("hint"); var interactions = new List(); @@ -224,9 +233,9 @@ private IAIInteraction DecodeInteraction(JObject json) }, "runtimeMessage" => new AIInteractionRuntimeMessage { - Severity = Enum.TryParse(json.Value("severity") ?? string.Empty, out var sev) ? sev : SmartHopper.Infrastructure.Diagnostics.SHRuntimeMessageSeverity.Info, - Code = Enum.TryParse(json.Value("code") ?? string.Empty, out var code) ? code : SmartHopper.Infrastructure.Diagnostics.SHMessageCode.Unknown, - Origin = Enum.TryParse(json.Value("origin") ?? string.Empty, out var orig) ? orig : SmartHopper.Infrastructure.Diagnostics.SHRuntimeMessageOrigin.Worker, + Severity = Enum.TryParse(json.Value("severity") ?? string.Empty, out var sev) ? sev : SmartHopper.ProviderSdk.Diagnostics.SHRuntimeMessageSeverity.Info, + Code = Enum.TryParse(json.Value("code") ?? string.Empty, out var code) ? code : SmartHopper.ProviderSdk.Diagnostics.SHMessageCode.Unknown, + Origin = Enum.TryParse(json.Value("origin") ?? string.Empty, out var orig) ? orig : SmartHopper.ProviderSdk.Diagnostics.SHRuntimeMessageOrigin.Worker, Surfaceable = json.Value("surfaceable"), Content = json.Value("content") }, diff --git a/src/SmartHopper.Core/Models/AIInputPayload.cs b/src/SmartHopper.Core/Models/AIInputPayload.cs index 903535360..c58b749d3 100644 --- a/src/SmartHopper.Core/Models/AIInputPayload.cs +++ b/src/SmartHopper.Core/Models/AIInputPayload.cs @@ -20,9 +20,10 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.Models { @@ -245,16 +246,16 @@ public static AIInputPayload FromAudio(AIInteractionAudio interaction) } /// - /// Creates a speech payload from a single audio interaction. + /// Creates an audio payload from a single speech/audio interaction. /// /// The audio interaction containing speech data. - /// A new AIInputPayload containing the speech interaction. + /// A new AIInputPayload containing the speech/audio interaction. public static AIInputPayload FromSpeech(AIInteractionAudio interaction) { return new AIInputPayload( new List { interaction }, AICapability.SpeechInput, - AIInputPayloadType.Speech, + AIInputPayloadType.Audio, interaction?.MimeType); } @@ -288,12 +289,9 @@ public enum AIInputPayloadType /// Image content payload. Image, - /// Audio content payload (music, sound effects, general audio). + /// Audio content payload (music, sound effects, speech, TTS, STT). Audio, - /// Speech content payload (voice, TTS, STT). - Speech, - /// Context filter payload (not rendered as content). Context, diff --git a/src/SmartHopper.Core/Parameters/VersatileImageParameter.cs b/src/SmartHopper.Core/Parameters/VersatileImageParameter.cs index 44a12af37..35c47586f 100644 --- a/src/SmartHopper.Core/Parameters/VersatileImageParameter.cs +++ b/src/SmartHopper.Core/Parameters/VersatileImageParameter.cs @@ -16,9 +16,9 @@ * along with this library; if not, see . */ +using System; using Grasshopper.Kernel; using SmartHopper.Core.Types; -using System; namespace SmartHopper.Core.Parameters { diff --git a/src/SmartHopper.Core/SmartHopper.Core.csproj b/src/SmartHopper.Core/SmartHopper.Core.csproj index 2f497ac3f..3cd631e14 100644 --- a/src/SmartHopper.Core/SmartHopper.Core.csproj +++ b/src/SmartHopper.Core/SmartHopper.Core.csproj @@ -60,8 +60,8 @@ - - + + diff --git a/src/SmartHopper.Core/Types/AIInputPayloadMerger.cs b/src/SmartHopper.Core/Types/AIInputPayloadMerger.cs index cf187697b..cb0bfba5a 100644 --- a/src/SmartHopper.Core/Types/AIInputPayloadMerger.cs +++ b/src/SmartHopper.Core/Types/AIInputPayloadMerger.cs @@ -22,8 +22,8 @@ using System.Linq; using Grasshopper.Kernel.Data; using SmartHopper.Core.Models; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.Types { diff --git a/src/SmartHopper.Core/Types/AIInputPayloadRenderer.cs b/src/SmartHopper.Core/Types/AIInputPayloadRenderer.cs index 2ced90693..2705c7f21 100644 --- a/src/SmartHopper.Core/Types/AIInputPayloadRenderer.cs +++ b/src/SmartHopper.Core/Types/AIInputPayloadRenderer.cs @@ -20,8 +20,8 @@ using System.Linq; using System.Text; using SmartHopper.Core.Models; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.Types { @@ -49,7 +49,6 @@ public static string RenderToUserText(AIInputPayload payload) AIInputPayloadType.Text => RenderTextPayload(payload), AIInputPayloadType.Image => RenderImagePayload(payload), AIInputPayloadType.Audio => RenderAudioPayload(payload), - AIInputPayloadType.Speech => RenderSpeechPayload(payload), AIInputPayloadType.Context => RenderContextPayload(payload), _ => RenderUnknownPayload(payload), }; @@ -119,7 +118,7 @@ private static string RenderImagePayload(AIInputPayload payload) /// /// Renders an audio payload to user-readable format. - /// Shows audio file information. + /// Shows audio or speech file information. /// private static string RenderAudioPayload(AIInputPayload payload) { @@ -155,44 +154,6 @@ private static string RenderAudioPayload(AIInputPayload payload) return sb.ToString().TrimEnd(); } - /// - /// Renders a speech payload to user-readable format. - /// Shows speech/voice file information. - /// - private static string RenderSpeechPayload(AIInputPayload payload) - { - var sb = new StringBuilder(); - sb.AppendLine("[Speech Payload]"); - - var speechCount = 0; - foreach (var interaction in payload.Interactions) - { - if (interaction is AIInteractionAudio speechInteraction) - { - speechCount++; - var source = !string.IsNullOrWhiteSpace(speechInteraction.FilePath) - ? $"File: {speechInteraction.FilePath}" - : speechInteraction.Data != null - ? $"In-memory ({speechInteraction.Data.Length} bytes)" - : "Unknown source"; - - var mime = speechInteraction.MimeType ?? "unknown"; - var lang = !string.IsNullOrWhiteSpace(speechInteraction.LanguageHint) - ? $" [{speechInteraction.LanguageHint}]" - : string.Empty; - - sb.AppendLine($"Speech {speechCount}: {source} ({mime}){lang}"); - } - } - - if (speechCount == 0) - { - sb.AppendLine("(No speech data found)"); - } - - return sb.ToString().TrimEnd(); - } - /// /// Renders a context payload to user-readable format. /// Shows context provider filter. @@ -269,7 +230,6 @@ public static string GetSummary(AIInputPayload payload) AIInputPayloadType.Text => $"Text ({interactionCount})", AIInputPayloadType.Image => $"Image ({interactionCount})", AIInputPayloadType.Audio => $"Audio ({interactionCount})", - AIInputPayloadType.Speech => $"Speech ({interactionCount})", AIInputPayloadType.Context => "Context", _ => $"Payload ({interactionCount})", }; diff --git a/src/SmartHopper.Core/Types/GH_AIInputPayload.cs b/src/SmartHopper.Core/Types/GH_AIInputPayload.cs index 5658be799..007a98027 100644 --- a/src/SmartHopper.Core/Types/GH_AIInputPayload.cs +++ b/src/SmartHopper.Core/Types/GH_AIInputPayload.cs @@ -19,7 +19,7 @@ using System; using Grasshopper.Kernel.Types; using SmartHopper.Core.Models; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.Types { diff --git a/src/SmartHopper.Core/Types/GH_AIRequestParameters.cs b/src/SmartHopper.Core/Types/GH_AIRequestParameters.cs index dfdb72fd4..19fd89bc0 100644 --- a/src/SmartHopper.Core/Types/GH_AIRequestParameters.cs +++ b/src/SmartHopper.Core/Types/GH_AIRequestParameters.cs @@ -22,7 +22,7 @@ using Grasshopper.Kernel.Types; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core; namespace SmartHopper.Core.Types { diff --git a/src/SmartHopper.Core/Types/VersatileImage.cs b/src/SmartHopper.Core/Types/VersatileImage.cs index 142c17c0d..78f796ecd 100644 --- a/src/SmartHopper.Core/Types/VersatileImage.cs +++ b/src/SmartHopper.Core/Types/VersatileImage.cs @@ -24,8 +24,8 @@ using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.Types { diff --git a/src/SmartHopper.Core/UI/CanvasButton.cs b/src/SmartHopper.Core/UI/CanvasButton.cs index 2b3f344d8..ef49e5d76 100644 --- a/src/SmartHopper.Core/UI/CanvasButton.cs +++ b/src/SmartHopper.Core/UI/CanvasButton.cs @@ -37,6 +37,9 @@ using SmartHopper.Infrastructure.Dialogs; using SmartHopper.Infrastructure.Properties; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Core.UI { diff --git a/src/SmartHopper.Core/UI/CanvasButtonBootstrap.cs b/src/SmartHopper.Core/UI/CanvasButtonBootstrap.cs index 123e5ba82..c77d763bf 100644 --- a/src/SmartHopper.Core/UI/CanvasButtonBootstrap.cs +++ b/src/SmartHopper.Core/UI/CanvasButtonBootstrap.cs @@ -20,6 +20,7 @@ using System.Runtime.CompilerServices; using Rhino; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Core.UI { diff --git a/src/SmartHopper.Core/UI/Chat/ChatResourceManager.cs b/src/SmartHopper.Core/UI/Chat/ChatResourceManager.cs index d72f1b08e..ede915243 100644 --- a/src/SmartHopper.Core/UI/Chat/ChatResourceManager.cs +++ b/src/SmartHopper.Core/UI/Chat/ChatResourceManager.cs @@ -34,8 +34,8 @@ using System.Text.RegularExpressions; using Markdig; using Markdig.Parsers; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.UI.Chat { diff --git a/src/SmartHopper.Core/UI/Chat/HtmlChatRenderer.cs b/src/SmartHopper.Core/UI/Chat/HtmlChatRenderer.cs index 9ee1d391f..fa8cf164a 100644 --- a/src/SmartHopper.Core/UI/Chat/HtmlChatRenderer.cs +++ b/src/SmartHopper.Core/UI/Chat/HtmlChatRenderer.cs @@ -25,7 +25,7 @@ using System.Diagnostics; using System.Globalization; using Markdig; -using SmartHopper.Infrastructure.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Core.UI.Chat { diff --git a/src/SmartHopper.Core/UI/Chat/WebChatDialog.cs b/src/SmartHopper.Core/UI/Chat/WebChatDialog.cs index 32e41c205..f77bd9567 100644 --- a/src/SmartHopper.Core/UI/Chat/WebChatDialog.cs +++ b/src/SmartHopper.Core/UI/Chat/WebChatDialog.cs @@ -33,15 +33,16 @@ using Newtonsoft.Json; using Rhino; using Rhino.UI; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Sessions; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Streaming; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Streaming; namespace SmartHopper.Core.UI.Chat { diff --git a/src/SmartHopper.Core/UI/Chat/WebChatObserver.cs b/src/SmartHopper.Core/UI/Chat/WebChatObserver.cs index 62840323b..d60a925fb 100644 --- a/src/SmartHopper.Core/UI/Chat/WebChatObserver.cs +++ b/src/SmartHopper.Core/UI/Chat/WebChatObserver.cs @@ -21,13 +21,14 @@ using System.Diagnostics; using System.Linq; using Rhino; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Sessions; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Core.UI.Chat { diff --git a/src/SmartHopper.Core/UI/Chat/WebChatUtils.Helpers.cs b/src/SmartHopper.Core/UI/Chat/WebChatUtils.Helpers.cs index db0f02dbd..04a55b664 100644 --- a/src/SmartHopper.Core/UI/Chat/WebChatUtils.Helpers.cs +++ b/src/SmartHopper.Core/UI/Chat/WebChatUtils.Helpers.cs @@ -19,10 +19,11 @@ using System; using System.Threading.Tasks; using Eto.Forms; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIModels; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Core.UI.Chat { diff --git a/src/SmartHopper.Core/UI/Chat/WebChatUtils.cs b/src/SmartHopper.Core/UI/Chat/WebChatUtils.cs index e6bd2bfad..f27fe54f0 100644 --- a/src/SmartHopper.Core/UI/Chat/WebChatUtils.cs +++ b/src/SmartHopper.Core/UI/Chat/WebChatUtils.cs @@ -26,8 +26,8 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.UI.Chat { diff --git a/src/SmartHopper.Core/UI/Chat/WebChatWorker.cs b/src/SmartHopper.Core/UI/Chat/WebChatWorker.cs index 4bb18fbec..e1896ee68 100644 --- a/src/SmartHopper.Core/UI/Chat/WebChatWorker.cs +++ b/src/SmartHopper.Core/UI/Chat/WebChatWorker.cs @@ -28,8 +28,8 @@ using System.Threading.Tasks; using Grasshopper; using Grasshopper.Kernel; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; namespace SmartHopper.Core.UI.Chat { diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/AIBatchTypesTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/AIBatchTypesTests.cs index b7b20472c..884e97131 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/AIBatchTypesTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/AIBatchTypesTests.cs @@ -20,8 +20,8 @@ namespace SmartHopper.Infrastructure.Tests.AICall { using System; using System.Collections.Generic; - using SmartHopper.Infrastructure.AICall.Batch; - using SmartHopper.Infrastructure.Diagnostics; + using SmartHopper.ProviderSdk.AICall.Batch; + using SmartHopper.ProviderSdk.Diagnostics; using Xunit; public class AIBatchSubmissionTests diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/AIInteractionImageTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/AIInteractionImageTests.cs index 31386a44c..a8077f901 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/AIInteractionImageTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/AIInteractionImageTests.cs @@ -19,7 +19,7 @@ namespace SmartHopper.Infrastructure.Tests.AICall { using System; - using SmartHopper.Infrastructure.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; using Xunit; public class AIInteractionImageTests diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/AIRequestCallTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/AIRequestCallTests.cs index d24eaec6b..2a2f6aaf4 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/AIRequestCallTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/AIRequestCallTests.cs @@ -21,11 +21,12 @@ namespace SmartHopper.Infrastructure.Tests.AICall using System; using System.Collections.Generic; using System.Linq; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; using SmartHopper.Infrastructure.AIModels; - using SmartHopper.Infrastructure.Diagnostics; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.Diagnostics; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/AIRequestParametersTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/AIRequestParametersTests.cs index 307c5d0c4..07f9898bc 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/AIRequestParametersTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/AIRequestParametersTests.cs @@ -20,7 +20,7 @@ namespace SmartHopper.Infrastructure.Tests.AICall { using System.Collections.Generic; using Newtonsoft.Json.Linq; - using SmartHopper.Infrastructure.AICall.Core; + using SmartHopper.ProviderSdk.AICall.Core; using Xunit; public class AIRequestParametersTests diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/AIReturnTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/AIReturnTests.cs index b739bccba..38eb947d4 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/AIReturnTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/AIReturnTests.cs @@ -21,12 +21,12 @@ namespace SmartHopper.Infrastructure.Tests.AICall using System; using System.Collections.Generic; using System.Linq; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; - using SmartHopper.Infrastructure.AICall.Metrics; - using SmartHopper.Infrastructure.Diagnostics; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.Diagnostics; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/Execution/DefaultProviderExecutorTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/Execution/DefaultProviderExecutorTests.cs index 09f1b5fa5..000081f72 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/Execution/DefaultProviderExecutorTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/Execution/DefaultProviderExecutorTests.cs @@ -21,13 +21,14 @@ namespace SmartHopper.Infrastructure.Tests.AICall.Execution using System; using System.Threading; using System.Threading.Tasks; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Execution; using SmartHopper.Infrastructure.AICall.Tools; - using SmartHopper.Infrastructure.Streaming; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.Diagnostics; + using SmartHopper.ProviderSdk.Streaming; using Xunit; /// @@ -148,9 +149,9 @@ private sealed class TestableRequest : AIRequestCall { public int DelayMs { get; set; } - public override (bool IsValid, System.Collections.Generic.List Errors) IsValid() + public override (bool IsValid, System.Collections.Generic.List Errors) IsValid() { - return (true, new System.Collections.Generic.List()); + return (true, new System.Collections.Generic.List()); } public override async Task Exec(bool stream = false, CancellationToken cancellationToken = default) diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/Policies/PolicyPipelineTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/Policies/PolicyPipelineTests.cs index f8461e41f..a6162b3f3 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/Policies/PolicyPipelineTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/Policies/PolicyPipelineTests.cs @@ -23,15 +23,15 @@ namespace SmartHopper.Infrastructure.Tests.AICall.Policies using System.Linq; using System.Threading; using System.Threading.Tasks; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Policies; using SmartHopper.Infrastructure.AICall.Policies.Request; using SmartHopper.Infrastructure.AICall.Policies.Response; using SmartHopper.Infrastructure.AICall.Tools; - using SmartHopper.Infrastructure.Diagnostics; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.Diagnostics; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/Providers/ProviderBatchParsingTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/Providers/ProviderBatchParsingTests.cs index 393372167..5ab42f627 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/Providers/ProviderBatchParsingTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/Providers/ProviderBatchParsingTests.cs @@ -22,12 +22,12 @@ namespace SmartHopper.Infrastructure.Tests.AICall.Providers { using System.Collections.Generic; using System.Linq; - using SmartHopper.Infrastructure.AICall.Batch; - using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Providers.Anthropic; using SmartHopper.Providers.Gemini; using SmartHopper.Providers.MistralAI; using SmartHopper.Providers.OpenAI; + using SmartHopper.ProviderSdk.AICall.Batch; + using SmartHopper.ProviderSdk.Diagnostics; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/Sessions/ConversationSessionTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/Sessions/ConversationSessionTests.cs index 5ac5faa4c..1e0cfc2d6 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/Sessions/ConversationSessionTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/Sessions/ConversationSessionTests.cs @@ -23,15 +23,15 @@ namespace SmartHopper.Infrastructure.Tests.AICall.Sessions using System.Linq; using System.Threading; using System.Threading.Tasks; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Execution; using SmartHopper.Infrastructure.AICall.Sessions; using SmartHopper.Infrastructure.AICall.Tools; - using SmartHopper.Infrastructure.Diagnostics; - using SmartHopper.Infrastructure.Streaming; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.Diagnostics; + using SmartHopper.ProviderSdk.Streaming; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/AICall/Utilities/InteractionUtilityTests.cs b/src/SmartHopper.Infrastructure.Tests/AICall/Utilities/InteractionUtilityTests.cs index ced789e5f..aa78fdd4d 100644 --- a/src/SmartHopper.Infrastructure.Tests/AICall/Utilities/InteractionUtilityTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AICall/Utilities/InteractionUtilityTests.cs @@ -20,9 +20,10 @@ namespace SmartHopper.Infrastructure.Tests.AICall.Utilities { using System.Collections.Generic; using System.Linq; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Utilities; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Utilities; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/AIToolManagerExecutionTests.cs b/src/SmartHopper.Infrastructure.Tests/AIToolManagerExecutionTests.cs index 81c9bcd8f..12d9dcbfd 100644 --- a/src/SmartHopper.Infrastructure.Tests/AIToolManagerExecutionTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AIToolManagerExecutionTests.cs @@ -20,12 +20,13 @@ namespace SmartHopper.Infrastructure.Tests { using System.Threading.Tasks; using Newtonsoft.Json.Linq; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.Diagnostics; using Xunit; /// @@ -64,7 +65,7 @@ public async Task ExecuteTool_InvalidToolCall_ReturnsError() var result = await AIToolManager.ExecuteTool(toolCall).ConfigureAwait(false); Assert.NotNull(result); - Assert.True(result.Messages.Exists(m => m.Severity == Diagnostics.SHRuntimeMessageSeverity.Error)); + Assert.True(result.Messages.Exists(m => m.Severity == SHRuntimeMessageSeverity.Error)); } #endregion @@ -145,7 +146,7 @@ public async Task ExecuteTool_UnknownTool_ReturnsError() var result = await AIToolManager.ExecuteTool(toolCall).ConfigureAwait(false); Assert.NotNull(result); - Assert.True(result.Messages.Exists(m => m.Severity == Diagnostics.SHRuntimeMessageSeverity.Error)); + Assert.True(result.Messages.Exists(m => m.Severity == SHRuntimeMessageSeverity.Error)); } #endregion diff --git a/src/SmartHopper.Infrastructure.Tests/AIToolManagerTests.cs b/src/SmartHopper.Infrastructure.Tests/AIToolManagerTests.cs index d6316c400..d790d1d09 100644 --- a/src/SmartHopper.Infrastructure.Tests/AIToolManagerTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AIToolManagerTests.cs @@ -19,8 +19,8 @@ namespace SmartHopper.Infrastructure.Tests { using System.Threading.Tasks; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AITools; + using SmartHopper.ProviderSdk.AICall.Core.Returns; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/AdvancedConfigTests.cs b/src/SmartHopper.Infrastructure.Tests/AdvancedConfigTests.cs index d74aacad1..7e194f9c4 100644 --- a/src/SmartHopper.Infrastructure.Tests/AdvancedConfigTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/AdvancedConfigTests.cs @@ -24,13 +24,16 @@ namespace SmartHopper.Infrastructure.Tests using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; - using SmartHopper.Infrastructure.Streaming; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.AIProviders; + using SmartHopper.ProviderSdk.Settings; + using SmartHopper.ProviderSdk.Streaming; using Xunit; public class AdvancedConfigTests diff --git a/src/SmartHopper.Infrastructure.Tests/InteractionKeyGenerationTests.cs b/src/SmartHopper.Infrastructure.Tests/InteractionKeyGenerationTests.cs index 215d16c55..8a1923283 100644 --- a/src/SmartHopper.Infrastructure.Tests/InteractionKeyGenerationTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/InteractionKeyGenerationTests.cs @@ -20,9 +20,9 @@ namespace SmartHopper.Infrastructure.Tests { using System; using Newtonsoft.Json.Linq; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.Diagnostics; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.Diagnostics; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/Mcp/AIToolMcpAdapterTests.cs b/src/SmartHopper.Infrastructure.Tests/Mcp/AIToolMcpAdapterTests.cs index f1218540e..0f6869fae 100644 --- a/src/SmartHopper.Infrastructure.Tests/Mcp/AIToolMcpAdapterTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/Mcp/AIToolMcpAdapterTests.cs @@ -22,11 +22,11 @@ namespace SmartHopper.Infrastructure.Tests.Mcp using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; using SmartHopper.Infrastructure.Mcp; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Returns; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/Mcp/JsonRpcDispatcherTests.cs b/src/SmartHopper.Infrastructure.Tests/Mcp/JsonRpcDispatcherTests.cs index a698c6c21..ad4f87b40 100644 --- a/src/SmartHopper.Infrastructure.Tests/Mcp/JsonRpcDispatcherTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/Mcp/JsonRpcDispatcherTests.cs @@ -21,11 +21,11 @@ namespace SmartHopper.Infrastructure.Tests.Mcp using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json.Linq; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; using SmartHopper.Infrastructure.Mcp; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Returns; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/ModelManagerTests.cs b/src/SmartHopper.Infrastructure.Tests/ModelManagerTests.cs index 8f34c801c..9c41d0158 100644 --- a/src/SmartHopper.Infrastructure.Tests/ModelManagerTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/ModelManagerTests.cs @@ -22,6 +22,7 @@ namespace SmartHopper.Infrastructure.Tests using System.Globalization; using System.Reflection; using SmartHopper.Infrastructure.AIModels; + using SmartHopper.ProviderSdk.AIModels; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/ProviderHashVerifierTests.cs b/src/SmartHopper.Infrastructure.Tests/ProviderHashVerifierTests.cs index b6c50f991..ea546766e 100644 --- a/src/SmartHopper.Infrastructure.Tests/ProviderHashVerifierTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/ProviderHashVerifierTests.cs @@ -21,6 +21,7 @@ namespace SmartHopper.Infrastructure.Tests using System.IO; using System.Text; using SmartHopper.Infrastructure.AIProviders; + using SmartHopper.ProviderSdk.AIProviders; using Xunit; public class ProviderHashVerifierTests diff --git a/src/SmartHopper.Infrastructure.Tests/ProviderManagerSignatureTests.cs b/src/SmartHopper.Infrastructure.Tests/ProviderManagerSignatureTests.cs index 5c493c929..3f7bd16ba 100644 --- a/src/SmartHopper.Infrastructure.Tests/ProviderManagerSignatureTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/ProviderManagerSignatureTests.cs @@ -30,6 +30,7 @@ namespace SmartHopper.Infrastructure.Tests using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using SmartHopper.Infrastructure.AIProviders; + using SmartHopper.ProviderSdk.AIProviders; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; diff --git a/src/SmartHopper.Infrastructure.Tests/ProviderManagerTests.cs b/src/SmartHopper.Infrastructure.Tests/ProviderManagerTests.cs index 2dc238dd1..ccaeb05a1 100644 --- a/src/SmartHopper.Infrastructure.Tests/ProviderManagerTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/ProviderManagerTests.cs @@ -19,6 +19,7 @@ namespace SmartHopper.Infrastructure.Tests { using SmartHopper.Infrastructure.AIProviders; + using SmartHopper.ProviderSdk.AIProviders; using Xunit; public class ProviderManagerTests diff --git a/src/SmartHopper.Infrastructure.Tests/SmartHopperSettingsTests.cs b/src/SmartHopper.Infrastructure.Tests/SmartHopperSettingsTests.cs index c1baf1062..fd8a06e56 100644 --- a/src/SmartHopper.Infrastructure.Tests/SmartHopperSettingsTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/SmartHopperSettingsTests.cs @@ -22,6 +22,7 @@ namespace SmartHopper.Infrastructure.Tests using System.IO; using Newtonsoft.Json; using SmartHopper.Infrastructure.Settings; + using SmartHopper.ProviderSdk.Settings; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/Utilities/JsonFormatHelperTests.cs b/src/SmartHopper.Infrastructure.Tests/Utilities/JsonFormatHelperTests.cs index 85f198f26..de179639e 100644 --- a/src/SmartHopper.Infrastructure.Tests/Utilities/JsonFormatHelperTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/Utilities/JsonFormatHelperTests.cs @@ -20,7 +20,7 @@ namespace SmartHopper.Infrastructure.Tests.Utilities { using System.Linq; using Newtonsoft.Json.Linq; - using SmartHopper.Infrastructure.Utilities; + using SmartHopper.ProviderSdk.Utilities; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/Utils/FilterTests.cs b/src/SmartHopper.Infrastructure.Tests/Utils/FilterTests.cs index d1789f965..955e1948d 100644 --- a/src/SmartHopper.Infrastructure.Tests/Utils/FilterTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/Utils/FilterTests.cs @@ -19,6 +19,7 @@ namespace SmartHopper.Infrastructure.Tests.Utils { using SmartHopper.Infrastructure.Utils; + using SmartHopper.ProviderSdk.Utils; using Xunit; /// diff --git a/src/SmartHopper.Infrastructure.Tests/VersionHelperTests.cs b/src/SmartHopper.Infrastructure.Tests/VersionHelperTests.cs index 4e05c34e7..f713fb54f 100644 --- a/src/SmartHopper.Infrastructure.Tests/VersionHelperTests.cs +++ b/src/SmartHopper.Infrastructure.Tests/VersionHelperTests.cs @@ -19,6 +19,7 @@ namespace SmartHopper.Infrastructure.Tests { using SmartHopper.Infrastructure.Utils; + using SmartHopper.ProviderSdk.Utils; using Xunit; public class VersionHelperTests diff --git a/src/SmartHopper.Infrastructure/AICall/Execution/DefaultProviderExecutor.cs b/src/SmartHopper.Infrastructure/AICall/Execution/DefaultProviderExecutor.cs index 5525ca390..fb010c401 100644 --- a/src/SmartHopper.Infrastructure/AICall/Execution/DefaultProviderExecutor.cs +++ b/src/SmartHopper.Infrastructure/AICall/Execution/DefaultProviderExecutor.cs @@ -20,11 +20,10 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Streaming; - +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Streaming; namespace SmartHopper.Infrastructure.AICall.Execution { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Execution/IProviderExecutor.cs b/src/SmartHopper.Infrastructure/AICall/Execution/IProviderExecutor.cs index 650fa2048..4104904b8 100644 --- a/src/SmartHopper.Infrastructure/AICall/Execution/IProviderExecutor.cs +++ b/src/SmartHopper.Infrastructure/AICall/Execution/IProviderExecutor.cs @@ -18,11 +18,10 @@ using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Streaming; - +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Streaming; namespace SmartHopper.Infrastructure.AICall.Execution { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Fallback/AudioToTextFallback.cs b/src/SmartHopper.Infrastructure/AICall/Fallback/AudioToTextFallback.cs index 56e4ae8b9..72c391e68 100644 --- a/src/SmartHopper.Infrastructure/AICall/Fallback/AudioToTextFallback.cs +++ b/src/SmartHopper.Infrastructure/AICall/Fallback/AudioToTextFallback.cs @@ -21,12 +21,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AIModels; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Infrastructure.AICall.Fallback { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Fallback/FallbackChain.cs b/src/SmartHopper.Infrastructure/AICall/Fallback/FallbackChain.cs index effa59a98..acc2140c7 100644 --- a/src/SmartHopper.Infrastructure/AICall/Fallback/FallbackChain.cs +++ b/src/SmartHopper.Infrastructure/AICall/Fallback/FallbackChain.cs @@ -19,9 +19,9 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; - +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Infrastructure.AICall.Fallback { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Fallback/FallbackProviderPin.cs b/src/SmartHopper.Infrastructure/AICall/Fallback/FallbackProviderPin.cs index 1b86fa462..07ef0c75c 100644 --- a/src/SmartHopper.Infrastructure/AICall/Fallback/FallbackProviderPin.cs +++ b/src/SmartHopper.Infrastructure/AICall/Fallback/FallbackProviderPin.cs @@ -17,7 +17,6 @@ */ using Newtonsoft.Json; - namespace SmartHopper.Infrastructure.AICall.Fallback { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Fallback/IModalityFallback.cs b/src/SmartHopper.Infrastructure/AICall/Fallback/IModalityFallback.cs index 15f43060d..369fa9eb2 100644 --- a/src/SmartHopper.Infrastructure/AICall/Fallback/IModalityFallback.cs +++ b/src/SmartHopper.Infrastructure/AICall/Fallback/IModalityFallback.cs @@ -18,9 +18,9 @@ using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; - +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Infrastructure.AICall.Fallback { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Fallback/ImageToTextFallback.cs b/src/SmartHopper.Infrastructure/AICall/Fallback/ImageToTextFallback.cs index 6d5210826..9c2e34f50 100644 --- a/src/SmartHopper.Infrastructure/AICall/Fallback/ImageToTextFallback.cs +++ b/src/SmartHopper.Infrastructure/AICall/Fallback/ImageToTextFallback.cs @@ -21,12 +21,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AIModels; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Infrastructure.AICall.Fallback { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Fallback/ModalityFallbackResolver.cs b/src/SmartHopper.Infrastructure/AICall/Fallback/ModalityFallbackResolver.cs index 4f0b5f1e0..19dc01665 100644 --- a/src/SmartHopper.Infrastructure/AICall/Fallback/ModalityFallbackResolver.cs +++ b/src/SmartHopper.Infrastructure/AICall/Fallback/ModalityFallbackResolver.cs @@ -23,7 +23,9 @@ using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; - +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Infrastructure.AICall.Fallback { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Fallback/ModalityFallbackResult.cs b/src/SmartHopper.Infrastructure/AICall/Fallback/ModalityFallbackResult.cs index b1507086d..61fc94100 100644 --- a/src/SmartHopper.Infrastructure/AICall/Fallback/ModalityFallbackResult.cs +++ b/src/SmartHopper.Infrastructure/AICall/Fallback/ModalityFallbackResult.cs @@ -17,10 +17,9 @@ */ using System.Collections.Generic; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Fallback { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/IRequestPolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/IRequestPolicy.cs index 2c20fcbfd..68c72eea5 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/IRequestPolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/IRequestPolicy.cs @@ -17,7 +17,6 @@ */ using System.Threading.Tasks; - namespace SmartHopper.Infrastructure.AICall.Policies { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/IResponsePolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/IResponsePolicy.cs index 263639de6..717eff66c 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/IResponsePolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/IResponsePolicy.cs @@ -17,7 +17,6 @@ */ using System.Threading.Tasks; - namespace SmartHopper.Infrastructure.AICall.Policies { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/PolicyContext.cs b/src/SmartHopper.Infrastructure/AICall/Policies/PolicyContext.cs index e7a880fd9..29e93d412 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/PolicyContext.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/PolicyContext.cs @@ -17,12 +17,12 @@ */ using System.Collections.Generic; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Policies { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/PolicyPipeline.cs b/src/SmartHopper.Infrastructure/AICall/Policies/PolicyPipeline.cs index e646eefa0..7b6dec9e4 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/PolicyPipeline.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/PolicyPipeline.cs @@ -21,14 +21,13 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Policies.Request; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Policies { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/Request/AIToolValidationRequestPolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/Request/AIToolValidationRequestPolicy.cs index 092cb2f4f..8033ca556 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/Request/AIToolValidationRequestPolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/Request/AIToolValidationRequestPolicy.cs @@ -19,11 +19,10 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Validation; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Policies.Request { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/Request/ContextInjectionRequestPolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/Request/ContextInjectionRequestPolicy.cs index 0b942c814..a631def78 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/Request/ContextInjectionRequestPolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/Request/ContextInjectionRequestPolicy.cs @@ -19,10 +19,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIContext; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; namespace SmartHopper.Infrastructure.AICall.Policies.Request { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/Request/RequestTimeoutPolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/Request/RequestTimeoutPolicy.cs index 96c2452c0..2eb545b2b 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/Request/RequestTimeoutPolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/Request/RequestTimeoutPolicy.cs @@ -17,10 +17,10 @@ */ using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.Settings; - +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Infrastructure.AICall.Policies.Request { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/Request/SchemaAttachRequestPolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/Request/SchemaAttachRequestPolicy.cs index 4288cc94a..31074bc32 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/Request/SchemaAttachRequestPolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/Request/SchemaAttachRequestPolicy.cs @@ -19,9 +19,8 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.JsonSchemas; - +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; namespace SmartHopper.Infrastructure.AICall.Policies.Request { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/Request/SchemaValidateRequestPolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/Request/SchemaValidateRequestPolicy.cs index 66941a628..86fcc442e 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/Request/SchemaValidateRequestPolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/Request/SchemaValidateRequestPolicy.cs @@ -19,10 +19,9 @@ using System; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.JsonSchemas; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Policies.Request { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/Request/ToolFilterNormalizationRequestPolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/Request/ToolFilterNormalizationRequestPolicy.cs index 967ec4e69..fa5892e31 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/Request/ToolFilterNormalizationRequestPolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/Request/ToolFilterNormalizationRequestPolicy.cs @@ -20,9 +20,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.Utils; - +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Infrastructure.AICall.Policies.Request { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/Response/FinishReasonNormalizeResponsePolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/Response/FinishReasonNormalizeResponsePolicy.cs index b130b49e8..a63690225 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/Response/FinishReasonNormalizeResponsePolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/Response/FinishReasonNormalizeResponsePolicy.cs @@ -20,11 +20,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Policies.Response { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Policies/Response/SchemaValidateResponsePolicy.cs b/src/SmartHopper.Infrastructure/AICall/Policies/Response/SchemaValidateResponsePolicy.cs index 8ce72cc06..5e01a2097 100644 --- a/src/SmartHopper.Infrastructure/AICall/Policies/Response/SchemaValidateResponsePolicy.cs +++ b/src/SmartHopper.Infrastructure/AICall/Policies/Response/SchemaValidateResponsePolicy.cs @@ -19,10 +19,9 @@ using System; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; using SmartHopper.Infrastructure.AICall.Validation; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Policies.Response { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.ContextManagement.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.ContextManagement.cs index ed9bc7551..9b723a2cc 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.ContextManagement.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.ContextManagement.cs @@ -24,13 +24,13 @@ namespace SmartHopper.Infrastructure.AICall.Sessions using System.Linq; using System.Threading; using System.Threading.Tasks; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Sessions.SpecialTurns; using SmartHopper.Infrastructure.AICall.Sessions.SpecialTurns.BuiltIn; using SmartHopper.Infrastructure.AIModels; - + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.AIModels; /// /// ConversationSession partial class containing context management and auto-summarization logic. /// diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.SpecialTurns.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.SpecialTurns.cs index b189314f7..b4ca18381 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.SpecialTurns.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.SpecialTurns.cs @@ -24,17 +24,18 @@ namespace SmartHopper.Infrastructure.AICall.Sessions using System.Linq; using System.Threading; using System.Threading.Tasks; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Execution; using SmartHopper.Infrastructure.AICall.Policies; using SmartHopper.Infrastructure.AICall.Sessions.SpecialTurns; using SmartHopper.Infrastructure.AICall.Utilities; using SmartHopper.Infrastructure.AIModels; - using SmartHopper.Infrastructure.Streaming; - + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.AICall.Utilities; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.Streaming; /// /// ConversationSession partial class containing special turn execution logic. /// @@ -78,21 +79,19 @@ public async Task ExecuteSpecialTurnAsync( // - session cancel token (this.cts.Token) // - external token passed to this method // - timeout via CancelAfter - using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(this.cts.Token, cancellationToken); - if (config.TimeoutMs.HasValue) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(this.cts.Token, cancellationToken)) { linkedCts.CancelAfter(config.TimeoutMs.Value); - } - - var effectiveCt = linkedCts.Token; + var effectiveCt = linkedCts.Token; - if (useStreaming) - { - result = await this.ExecuteStreamingSpecialTurnAsync(specialRequest, config, turnId, effectiveCt).ConfigureAwait(false); - } - else - { - result = await this.ExecuteNonStreamingSpecialTurnAsync(specialRequest, config, turnId, effectiveCt).ConfigureAwait(false); + if (useStreaming) + { + result = await this.ExecuteStreamingSpecialTurnAsync(specialRequest, config, turnId, effectiveCt).ConfigureAwait(false); + } + else + { + result = await this.ExecuteNonStreamingSpecialTurnAsync(specialRequest, config, turnId, effectiveCt).ConfigureAwait(false); + } } // Apply persistence strategy to main conversation (this is where observers get notified) diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.cs index dbc3f2f9c..2a80b3bf4 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.cs @@ -25,18 +25,19 @@ namespace SmartHopper.Infrastructure.AICall.Sessions using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Execution; - using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Policies; using SmartHopper.Infrastructure.AICall.Utilities; - using SmartHopper.Infrastructure.Diagnostics; using SmartHopper.Infrastructure.Settings; - using SmartHopper.Infrastructure.Streaming; - + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.AICall.Utilities; + using SmartHopper.ProviderSdk.Diagnostics; + using SmartHopper.ProviderSdk.Settings; + using SmartHopper.ProviderSdk.Streaming; /// /// Conversation session that manages chat history and orchestrates multi-turn conversations. /// Handles both streaming and non-streaming execution with proper history management. diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSessionHelpers.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSessionHelpers.cs index 832edc1c5..452099451 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSessionHelpers.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSessionHelpers.cs @@ -27,15 +27,15 @@ namespace SmartHopper.Infrastructure.AICall.Sessions using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; - using SmartHopper.Infrastructure.AICall.Metrics; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AICall.Utilities; - using SmartHopper.Infrastructure.Diagnostics; - + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.AICall.Utilities; + using SmartHopper.ProviderSdk.Diagnostics; /// /// Helper methods supporting orchestration logic. /// diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/IConversationObserver.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/IConversationObserver.cs index 0f031d745..636137dd9 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/IConversationObserver.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/IConversationObserver.cs @@ -17,13 +17,11 @@ */ using System; - namespace SmartHopper.Infrastructure.AICall.Sessions { - using SmartHopper.Infrastructure.AICall.Core.Interactions; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; - + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; /// /// Observer of conversation session lifecycle and streaming deltas. /// Provides comprehensive event notifications for all conversation stages. diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/IConversationSession.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/IConversationSession.cs index 6e3cfb62d..47c993e48 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/IConversationSession.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/IConversationSession.cs @@ -21,10 +21,9 @@ namespace SmartHopper.Infrastructure.AICall.Sessions using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; - using SmartHopper.Infrastructure.Streaming; - + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.Streaming; /// /// Contract for conversation session orchestrator. Minimal non-streaming API. /// diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/SessionOptions.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/SessionOptions.cs index 1c11ccec1..0e7eff63c 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/SessionOptions.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/SessionOptions.cs @@ -17,7 +17,6 @@ */ using System.Threading; - namespace SmartHopper.Infrastructure.AICall.Sessions { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/BuiltIn/GreetingSpecialTurn.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/BuiltIn/GreetingSpecialTurn.cs index 03f826568..fac766806 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/BuiltIn/GreetingSpecialTurn.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/BuiltIn/GreetingSpecialTurn.cs @@ -19,10 +19,10 @@ namespace SmartHopper.Infrastructure.AICall.Sessions.SpecialTurns.BuiltIn { using System.Collections.Generic; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; - + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AIModels; /// /// Factory for creating greeting special turn configurations. /// diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/BuiltIn/SummarizeSpecialTurn.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/BuiltIn/SummarizeSpecialTurn.cs index 5ddd3bd88..7652ffbb0 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/BuiltIn/SummarizeSpecialTurn.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/BuiltIn/SummarizeSpecialTurn.cs @@ -21,10 +21,10 @@ namespace SmartHopper.Infrastructure.AICall.Sessions.SpecialTurns.BuiltIn using System.Collections.Generic; using System.Linq; using System.Text; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; - + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AIModels; /// /// Factory for creating summarization special turn configurations. /// Used to compress long conversation histories when approaching context limits. diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/InteractionFilter.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/InteractionFilter.cs index 68528dad7..2e31d9261 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/InteractionFilter.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/InteractionFilter.cs @@ -19,9 +19,8 @@ namespace SmartHopper.Infrastructure.AICall.Sessions.SpecialTurns { using System.Collections.Generic; - using SmartHopper.Infrastructure.AICall.Core.Base; - using SmartHopper.Infrastructure.AICall.Core.Interactions; - + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; /// /// Filter to control which interaction types are included/excluded during persistence. /// Uses an allowlist/blocklist approach for flexible filtering. diff --git a/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/SpecialTurnConfig.cs b/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/SpecialTurnConfig.cs index dc70ccea9..51483ae26 100644 --- a/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/SpecialTurnConfig.cs +++ b/src/SmartHopper.Infrastructure/AICall/Sessions/SpecialTurns/SpecialTurnConfig.cs @@ -19,9 +19,9 @@ namespace SmartHopper.Infrastructure.AICall.Sessions.SpecialTurns { using System.Collections.Generic; - using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AIModels; - + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AIModels; /// /// Configuration for executing a special turn with custom overrides. /// Special turns are executed through the regular conversation flow but can override diff --git a/src/SmartHopper.Infrastructure/AICall/Tools/AIToolCall.cs b/src/SmartHopper.Infrastructure/AICall/Tools/AIToolCall.cs index e75a8a8cc..a861cfb3e 100644 --- a/src/SmartHopper.Infrastructure/AICall/Tools/AIToolCall.cs +++ b/src/SmartHopper.Infrastructure/AICall/Tools/AIToolCall.cs @@ -22,15 +22,14 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Validation; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Tools { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Tools/ToolCallResult.cs b/src/SmartHopper.Infrastructure/AICall/Tools/ToolCallResult.cs index 3c7041add..ca47c4acc 100644 --- a/src/SmartHopper.Infrastructure/AICall/Tools/ToolCallResult.cs +++ b/src/SmartHopper.Infrastructure/AICall/Tools/ToolCallResult.cs @@ -19,11 +19,10 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Tools { /// @@ -205,9 +204,16 @@ public static ToolCallResult FromAIReturn(AIReturn aiReturn, AIAgent agent = AIA } /// - /// Returns the JSON representation of , or an empty - /// string when the payload is null. + /// Allows a to be used wherever a is expected, + /// delegating to so legacy callers (e.g. RuntimeMessageUtility.ExtractMessages(toolResult)) + /// continue to compile against the Provider SDK surface. /// + /// The tool result to unwrap. + public static implicit operator JObject?(ToolCallResult? result) + { + return result?.Result; + } + /// The serialized payload. public override string ToString() { diff --git a/src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelope.cs b/src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelope.cs index b23b7920a..a1f95084f 100644 --- a/src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelope.cs +++ b/src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelope.cs @@ -20,7 +20,6 @@ using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; - namespace SmartHopper.Infrastructure.AICall.Tools { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelopeExtensions.cs b/src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelopeExtensions.cs index e757df1e1..b8d02154d 100644 --- a/src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelopeExtensions.cs +++ b/src/SmartHopper.Infrastructure/AICall/Tools/ToolResultEnvelopeExtensions.cs @@ -17,7 +17,6 @@ */ using Newtonsoft.Json.Linq; - namespace SmartHopper.Infrastructure.AICall.Tools { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Utilities/ToolCallResultRuntimeMessageExtensions.cs b/src/SmartHopper.Infrastructure/AICall/Utilities/ToolCallResultRuntimeMessageExtensions.cs new file mode 100644 index 000000000..dbaa20436 --- /dev/null +++ b/src/SmartHopper.Infrastructure/AICall/Utilities/ToolCallResultRuntimeMessageExtensions.cs @@ -0,0 +1,61 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System.Collections.Generic; +using SmartHopper.Infrastructure.AICall.Tools; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.Infrastructure.AICall.Utilities +{ + /// + /// Host-side extension that bridges (host-only) into + /// (SDK) for runtime-message extraction. + /// + public static class ToolCallResultRuntimeMessageExtensions + { + /// + /// Extracts runtime messages from a envelope. + /// Combines diagnostics carried on the envelope with messages found inside + /// the underlying JSON payload. + /// + /// The envelope to extract messages from. + /// A list of extracted SHRuntimeMessage objects, or empty list if none found. + public static List ExtractMessages(this ToolCallResult toolResult) + { + var messages = new List(); + if (toolResult == null) + { + return messages; + } + + if (toolResult.Messages != null) + { + foreach (var m in toolResult.Messages) + { + if (m != null) + { + messages.Add(m); + } + } + } + + messages.AddRange(RuntimeMessageUtility.ExtractMessages(toolResult.Result)); + return messages; + } + } +} diff --git a/src/SmartHopper.Infrastructure/AICall/Validation/ComponentCapabilityValidator.cs b/src/SmartHopper.Infrastructure/AICall/Validation/ComponentCapabilityValidator.cs index b0dccd8b0..d681d754c 100644 --- a/src/SmartHopper.Infrastructure/AICall/Validation/ComponentCapabilityValidator.cs +++ b/src/SmartHopper.Infrastructure/AICall/Validation/ComponentCapabilityValidator.cs @@ -20,12 +20,14 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; using SmartHopper.Infrastructure.AICall.Utilities; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Validation { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Validation/IValidator.cs b/src/SmartHopper.Infrastructure/AICall/Validation/IValidator.cs index d01a1c0dd..822ce9842 100644 --- a/src/SmartHopper.Infrastructure/AICall/Validation/IValidator.cs +++ b/src/SmartHopper.Infrastructure/AICall/Validation/IValidator.cs @@ -20,9 +20,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Validation { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Validation/JsonSchemaResponseValidator.cs b/src/SmartHopper.Infrastructure/AICall/Validation/JsonSchemaResponseValidator.cs index 7ebd65515..32bf42f64 100644 --- a/src/SmartHopper.Infrastructure/AICall/Validation/JsonSchemaResponseValidator.cs +++ b/src/SmartHopper.Infrastructure/AICall/Validation/JsonSchemaResponseValidator.cs @@ -20,13 +20,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.JsonSchemas; using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Validation { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Validation/ToolCapabilityValidator.cs b/src/SmartHopper.Infrastructure/AICall/Validation/ToolCapabilityValidator.cs index d1d605079..472d62934 100644 --- a/src/SmartHopper.Infrastructure/AICall/Validation/ToolCapabilityValidator.cs +++ b/src/SmartHopper.Infrastructure/AICall/Validation/ToolCapabilityValidator.cs @@ -19,13 +19,14 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Utilities; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Validation { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Validation/ToolExistsValidator.cs b/src/SmartHopper.Infrastructure/AICall/Validation/ToolExistsValidator.cs index 08038fab8..bb95319bb 100644 --- a/src/SmartHopper.Infrastructure/AICall/Validation/ToolExistsValidator.cs +++ b/src/SmartHopper.Infrastructure/AICall/Validation/ToolExistsValidator.cs @@ -19,12 +19,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; using SmartHopper.Infrastructure.AICall.Utilities; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Validation { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Validation/ToolJsonSchemaValidator.cs b/src/SmartHopper.Infrastructure/AICall/Validation/ToolJsonSchemaValidator.cs index 96e4505e0..9a617dbdc 100644 --- a/src/SmartHopper.Infrastructure/AICall/Validation/ToolJsonSchemaValidator.cs +++ b/src/SmartHopper.Infrastructure/AICall/Validation/ToolJsonSchemaValidator.cs @@ -21,13 +21,13 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.JsonSchemas; using SmartHopper.Infrastructure.AICall.Utilities; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AICall.Validation { /// diff --git a/src/SmartHopper.Infrastructure/AICall/Validation/ValidationContext.cs b/src/SmartHopper.Infrastructure/AICall/Validation/ValidationContext.cs index f242cb527..27faca8bb 100644 --- a/src/SmartHopper.Infrastructure/AICall/Validation/ValidationContext.cs +++ b/src/SmartHopper.Infrastructure/AICall/Validation/ValidationContext.cs @@ -18,10 +18,10 @@ using System; using System.Collections.Generic; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AIModels; - +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Infrastructure.AICall.Validation { /// diff --git a/src/SmartHopper.Infrastructure/AIContext/ContextManager.cs b/src/SmartHopper.Infrastructure/AIContext/ContextManager.cs index bef6b144c..b3184115f 100644 --- a/src/SmartHopper.Infrastructure/AIContext/ContextManager.cs +++ b/src/SmartHopper.Infrastructure/AIContext/ContextManager.cs @@ -21,7 +21,7 @@ using System.Diagnostics; using System.Linq; using SmartHopper.Infrastructure.Utils; - +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Infrastructure.AIContext { /// diff --git a/src/SmartHopper.Infrastructure/AIContext/IAIContextProvider.cs b/src/SmartHopper.Infrastructure/AIContext/IAIContextProvider.cs index 6de08d9ca..5f84623d4 100644 --- a/src/SmartHopper.Infrastructure/AIContext/IAIContextProvider.cs +++ b/src/SmartHopper.Infrastructure/AIContext/IAIContextProvider.cs @@ -17,7 +17,6 @@ */ using System.Collections.Generic; - namespace SmartHopper.Infrastructure.AIContext { /// diff --git a/src/SmartHopper.Infrastructure/AIModels/AIModelCapabilityRegistry.cs b/src/SmartHopper.Infrastructure/AIModels/AIModelCapabilityRegistry.cs deleted file mode 100644 index 7e70871d8..000000000 --- a/src/SmartHopper.Infrastructure/AIModels/AIModelCapabilityRegistry.cs +++ /dev/null @@ -1,151 +0,0 @@ -/* - * SmartHopper - AI-powered Grasshopper Plugin - * Copyright (C) 2024-2026 Marc Roca Musach - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library; if not, see . - */ - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace SmartHopper.Infrastructure.AIModels -{ - /// - /// Registry containing capability information for all known models. - /// Thread-safe: uses ConcurrentDictionary for reads/writes. - /// - public sealed class AIModelCapabilityRegistry - { - /// - /// Gets the dictionary of model capabilities keyed by "provider.model". - /// Thread-safe: uses ConcurrentDictionary for reads/writes. - /// - public ConcurrentDictionary Models { get; } = new ConcurrentDictionary(); - - /// - /// Gets capabilities for a specific model. - /// Enforces exact name or alias matching only. Wildcard patterns are not supported. - /// - /// The provider name. - /// The model name. - /// Model capabilities or null if not found. - public AIModelCapabilities GetCapabilities(string provider, string model) - { - var key = $"{provider?.ToLowerInvariant()}.{model?.ToLowerInvariant()}"; - Debug.WriteLine($"[GetCapabilities] Looking for '{key}'"); - - // Try exact match first (fastest path) - if (this.Models.TryGetValue(key, out var capabilities)) - { - Debug.WriteLine($"[GetCapabilities] Found exact match for '{key}' with capabilities {capabilities.Capabilities.ToDetailedString()}"); - return capabilities; - } - - // Fallback: search by alias within the same provider - var providerLower = provider?.ToLowerInvariant(); - var modelLower = model?.ToLowerInvariant(); - var byAlias = this.Models.Values - .Where(m => m != null && m.Provider.Equals(providerLower, StringComparison.OrdinalIgnoreCase)) - .FirstOrDefault(m => m.Aliases != null && m.Aliases.Any(a => string.Equals(a, modelLower, StringComparison.OrdinalIgnoreCase))); - - if (byAlias != null) - { - Debug.WriteLine($"[GetCapabilities] Found alias match for '{key}' -> '{byAlias.GetKey()}'"); - return byAlias; - } - - return null; - } - - /// - /// Adds or updates capabilities for a specific model. - /// - /// The model capabilities to add/update. - public void SetCapabilities(AIModelCapabilities capabilities) - { - if (capabilities == null) return; - - var key = capabilities.GetKey(); - this.Models[key] = capabilities; - } - - /// - /// Gets all models that support the specified capabilities. - /// - /// The required capabilities. - /// List of matching model capabilities. - public List FindModelsWithCapabilities(AICapability requiredCapabilities) - { - if (requiredCapabilities == AICapability.None) - { - return this.Models.Values.ToList(); - } - - return this.Models.Values - .Where(model => model.HasCapability(requiredCapabilities)) - .ToList(); - } - - /// - /// Gets the default model for a provider and specific capability. - /// Exact names only; no wildcard resolution. Prioritizes exact default flag; then any compatible default. - /// - /// The provider name. - /// The required capability. - /// The default model name or null if none found. - public string GetDefaultModel(string provider, AICapability requiredCapability = AICapability.Text2Text) - { - if (string.IsNullOrEmpty(provider)) - { - return null; - } - - var providerModels = this.Models.Values - .Where(m => m != null && string.Equals(m.Provider, provider, System.StringComparison.OrdinalIgnoreCase)) - .ToList(); - - Debug.WriteLine($"[ModelManager] Getting the default model among {providerModels.Count} models for {provider} with capability {requiredCapability.ToDetailedString()}"); - - if (!providerModels.Any()) - return null; - - // PRIORITY 1: Exact defaults for the required capability - var concreteExactModel = providerModels - .FirstOrDefault(m => (m.Default & requiredCapability) == requiredCapability); - - if (concreteExactModel != null) - { - Debug.WriteLine($"[ModelManager] Found concrete exact default model {concreteExactModel.Model} for {provider} with capability {requiredCapability.ToDetailedString()}"); - return concreteExactModel.Model; - } - - // PRIORITY 2: Any default that is compatible with the required capability - var concreteCompatibleModel = providerModels - .Where(m => m.Default != AICapability.None && m.HasCapability(requiredCapability)) - .FirstOrDefault(); - - if (concreteCompatibleModel != null) - { - Debug.WriteLine($"[ModelManager] Found concrete compatible default model {concreteCompatibleModel.Model} for {provider} with capability {requiredCapability.ToDetailedString()}"); - return concreteCompatibleModel.Model; - } - - Debug.WriteLine($"[ModelManager] No default model found for {provider} with capability {requiredCapability.ToDetailedString()}"); - return null; - } - } -} diff --git a/src/SmartHopper.Infrastructure/AIModels/ModelManager.cs b/src/SmartHopper.Infrastructure/AIModels/ModelManager.cs index 3cce31d57..ad6daf34f 100644 --- a/src/SmartHopper.Infrastructure/AIModels/ModelManager.cs +++ b/src/SmartHopper.Infrastructure/AIModels/ModelManager.cs @@ -20,7 +20,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; - +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Infrastructure.AIModels { /// diff --git a/src/SmartHopper.Infrastructure/AIProviders/ProviderAssemblyLoader.cs b/src/SmartHopper.Infrastructure/AIProviders/ProviderAssemblyLoader.cs new file mode 100644 index 000000000..00f71dd04 --- /dev/null +++ b/src/SmartHopper.Infrastructure/AIProviders/ProviderAssemblyLoader.cs @@ -0,0 +1,248 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Metadata; +namespace SmartHopper.Infrastructure.AIProviders +{ + /// + /// Custom used to load provider plug-in assemblies in + /// isolation while still binding their references to the host's copy of + /// and other shared dependencies. + /// + /// + /// Loading strategy: + /// + /// + /// Assemblies whose simple name is present in + /// are delegated to , so a provider's + /// IAIProvider resolves to the SAME type identity as the host expects. + /// This is what allows reflection-based plug-in dispatch to work across the ALC + /// boundary. + /// + /// + /// Any other DLL co-located with the provider (provider-private dependencies) + /// is loaded into this ALC, isolated from the host's load context. + /// + /// + /// Native libraries are also resolved relative to the provider directory. + /// + /// + /// + /// + /// + /// Combined with the type-identity check in , + /// this fulfils plan §4.5 (Type identity & dependency loading): providers built + /// against a mismatched copy of the SDK are rejected with a clear error rather than + /// silently failing at activation time. + /// + /// + internal sealed class ProviderLoadContext : AssemblyLoadContext + { + /// + /// Simple assembly names that MUST resolve to the host's copy (default ALC). + /// + private static readonly HashSet SharedAssemblyNames = new(StringComparer.OrdinalIgnoreCase) + { + "SmartHopper.ProviderSdk", + "SmartHopper.Infrastructure", + "Newtonsoft.Json", + "System.Drawing.Common", + }; + + private readonly AssemblyDependencyResolver _resolver; + private readonly string _providerDirectory; + + public ProviderLoadContext(string providerAssemblyPath) + : base(name: $"ProviderLoadContext::{Path.GetFileNameWithoutExtension(providerAssemblyPath)}", isCollectible: false) + { + this._resolver = new AssemblyDependencyResolver(providerAssemblyPath); + this._providerDirectory = Path.GetDirectoryName(providerAssemblyPath) + ?? throw new ArgumentException("Provider assembly path has no directory.", nameof(providerAssemblyPath)); + } + + /// + protected override Assembly Load(AssemblyName assemblyName) + { + // Shared types MUST come from the default ALC so type identity matches host. + if (assemblyName.Name != null && SharedAssemblyNames.Contains(assemblyName.Name)) + { + return null!; + } + + var resolved = this._resolver.ResolveAssemblyToPath(assemblyName); + if (!string.IsNullOrEmpty(resolved) && File.Exists(resolved)) + { + return this.LoadFromAssemblyPath(resolved); + } + + // Fallback: look in the provider directory. + var candidate = Path.Combine(this._providerDirectory, assemblyName.Name + ".dll"); + if (File.Exists(candidate)) + { + return this.LoadFromAssemblyPath(candidate); + } + + return null!; + } + + /// + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var path = this._resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + return string.IsNullOrEmpty(path) ? IntPtr.Zero : this.LoadUnmanagedDllFromPath(path); + } + } + + /// + /// Outcome of loading a provider assembly via . + /// + internal enum ProviderLoadOutcome + { + /// Assembly loaded successfully and SDK type identity matched the host. + Loaded, + + /// The file is not a valid managed assembly. + InvalidAssembly, + + /// The assembly was loaded but does not expose any IAIProviderFactory. + NoFactory, + + /// + /// The provider's SDK type identity does not match the host's SDK copy + /// (it was likely built against a different SDK version). + /// + SdkTypeIdentityMismatch, + + /// + /// The provider's / + /// metadata is incompatible with the host's SDK version. + /// + SdkVersionIncompatible, + } + + /// + /// Loads a provider assembly into an isolated , + /// verifies that its SDK type identity matches the host's copy, and returns the + /// loaded assembly plus an outcome value the caller can act on. + /// + internal static class ProviderAssemblyLoader + { + /// + /// Try to load into a per-provider ALC and validate + /// it against the host's SDK type identity. + /// + /// Full path to the provider DLL. + /// The loaded assembly on success; otherwise null. + /// A user-facing diagnostic message describing the outcome. + /// The outcome of the load attempt. + public static ProviderLoadOutcome TryLoad( + string assemblyPath, + out Assembly? assembly, + out string diagnostic) + { + assembly = null; + diagnostic = string.Empty; + + try + { + var alc = new ProviderLoadContext(assemblyPath); + assembly = alc.LoadFromAssemblyPath(assemblyPath); + } + catch (BadImageFormatException ex) + { + diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' is not a valid managed assembly: {ex.Message}"; + return ProviderLoadOutcome.InvalidAssembly; + } + catch (FileLoadException ex) + { + diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' failed to load: {ex.Message}"; + return ProviderLoadOutcome.InvalidAssembly; + } + catch (Exception ex) + { + diagnostic = $"Unexpected error loading provider '{Path.GetFileName(assemblyPath)}': {ex.Message}"; + return ProviderLoadOutcome.InvalidAssembly; + } + + // Type-identity check: any IAIProviderFactory exposed by the provider must + // refer to the SAME runtime type as the one in the host's loaded SDK. + Type[] exportedTypes; + try + { + exportedTypes = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + exportedTypes = ex.Types.Where(t => t != null).ToArray()!; + Debug.WriteLine($"[ProviderAssemblyLoader] ReflectionTypeLoadException in {Path.GetFileName(assemblyPath)}: {ex.Message}"); + } + + var factoryTypeFromHost = typeof(IAIProviderFactory); + var factoryFromAssembly = exportedTypes + .FirstOrDefault(t => t is not null && t.GetInterfaces().Any(i => + string.Equals(i.FullName, factoryTypeFromHost.FullName, StringComparison.Ordinal))); + + if (factoryFromAssembly is null) + { + diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' does not expose any type implementing IAIProviderFactory and will be ignored."; + return ProviderLoadOutcome.NoFactory; + } + + var sdkInterfaceSeenByProvider = factoryFromAssembly + .GetInterfaces() + .FirstOrDefault(i => string.Equals(i.FullName, factoryTypeFromHost.FullName, StringComparison.Ordinal)); + + if (sdkInterfaceSeenByProvider is null + || sdkInterfaceSeenByProvider.Assembly != factoryTypeFromHost.Assembly) + { + var hostSdk = factoryTypeFromHost.Assembly.GetName(); + var providerSdk = sdkInterfaceSeenByProvider?.Assembly.GetName(); + diagnostic = + $"Provider '{Path.GetFileName(assemblyPath)}' was built against a different SmartHopper.ProviderSdk and cannot be loaded.\n" + + $"Host SDK: {hostSdk.Name} v{hostSdk.Version}\n" + + $"Provider SDK: {(providerSdk == null ? "(unresolved)" : $"{providerSdk.Name} v{providerSdk.Version}")}"; + return ProviderLoadOutcome.SdkTypeIdentityMismatch; + } + + // SemVer-based compatibility check using BuiltAgainstSdk/MinHostSdk attributes. + // MissingMetadata is tolerated (logged via diagnostic but not blocking) because + // first-party providers ship without these attributes set explicitly today. + var compatResult = SdkCompatibility.Check(assembly, out var compatDiagnostic); + switch (compatResult) + { + case SdkCompatibility.CompatibilityResult.MajorMismatch: + case SdkCompatibility.CompatibilityResult.HostTooOld: + diagnostic = compatDiagnostic; + return ProviderLoadOutcome.SdkVersionIncompatible; + case SdkCompatibility.CompatibilityResult.MissingMetadata: + Debug.WriteLine($"[ProviderAssemblyLoader] {compatDiagnostic}"); + break; + } + + return ProviderLoadOutcome.Loaded; + } + } +} diff --git a/src/SmartHopper.Infrastructure/AIProviders/ProviderClassifier.cs b/src/SmartHopper.Infrastructure/AIProviders/ProviderClassifier.cs new file mode 100644 index 000000000..9bdff067c --- /dev/null +++ b/src/SmartHopper.Infrastructure/AIProviders/ProviderClassifier.cs @@ -0,0 +1,313 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using SmartHopper.Infrastructure.Utils; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Utils; +namespace SmartHopper.Infrastructure.AIProviders +{ + /// + /// High-level classification of a provider assembly as determined by the + /// cryptographic checks in . + /// + /// + /// Classification is based purely on strong-name token, Authenticode certificate + /// (Windows only), and the SmartHopper-managed SHA-256 hash manifest. It deliberately + /// ignores provider id, assembly file name, and any metadata claim — those can be + /// trivially spoofed and provide no security guarantee. + /// + /// + public enum ProviderClassification + { + /// + /// The provider is cryptographically attributable to SmartHopper itself: it + /// passes strong-name + Authenticode verification (where applicable) and/or + /// matches the official hash manifest, with no contradicting signals. + /// + Official, + + /// + /// The provider claims to be official by one signal (e.g. hash listed in the + /// manifest) but another signal contradicts it (e.g. signature mismatch, or + /// hash mismatch). Always blocked. + /// + OfficialTampered, + + /// + /// The provider is a valid managed assembly that is NOT cryptographically tied + /// to SmartHopper. Includes both signed-by-other-key and unsigned third-party + /// DLLs. + /// + Community, + + /// + /// The file is not a valid managed assembly, fails SDK type identity, has no + /// IAIProviderFactory, has malformed metadata, or could not be loaded. + /// Always blocked. + /// + Invalid, + } + + /// + /// Result of a single call. + /// + public sealed class ProviderClassificationResult + { + /// The final classification. + public ProviderClassification Classification { get; init; } + + /// The SHA-256 hash of the file (hex, lower-case), or null if not computed. + public string? Sha256 { get; init; } + + /// Strong-name public key token (hex), or null if unsigned/unreadable. + public string? StrongNameToken { get; init; } + + /// Whether the strong-name token matches the host's token. + public bool StrongNameMatchesHost { get; init; } + + /// Authenticode signer subject (Windows only), or null. + public string? AuthenticodeSubject { get; init; } + + /// Authenticode signer SHA-1 thumbprint (Windows only), or null. + public string? AuthenticodeThumbprint { get; init; } + + /// Whether the Authenticode signature matches the host's certificate. + public bool AuthenticodeMatchesHost { get; init; } + + /// Result of the official-hash-manifest lookup, when performed. + public ProviderVerificationStatus HashStatus { get; init; } + + /// Diagnostic message for logs and dialogs. + public string Diagnostic { get; init; } = string.Empty; + } + + /// + /// Cryptographic classifier for provider assemblies. Returns a + /// the caller can act on — it never + /// throws to block loading, and it never trusts file names or assembly metadata. + /// + public static class ProviderClassifier + { + /// + /// Classify the provider assembly at . + /// + /// Full path to the candidate provider DLL. + public static async Task ClassifyAsync(string assemblyPath) + { + if (assemblyPath is null) + { + throw new ArgumentNullException(nameof(assemblyPath)); + } + + if (!File.Exists(assemblyPath)) + { + return new ProviderClassificationResult + { + Classification = ProviderClassification.Invalid, + Diagnostic = $"Provider file not found: {assemblyPath}", + }; + } + + string? sha256 = null; + try + { + sha256 = ProviderHashVerifier.CalculateFileHash(assemblyPath); + } + catch (Exception ex) + { + Debug.WriteLine($"[ProviderClassifier] Failed to hash {assemblyPath}: {ex.Message}"); + } + + string? token = null; + bool tokenMatchesHost = false; + try + { + var asmName = AssemblyName.GetAssemblyName(assemblyPath); + var rawToken = asmName.GetPublicKeyToken(); + if (rawToken is { Length: > 0 }) + { + token = BitConverter.ToString(rawToken).Replace("-", string.Empty).ToLowerInvariant(); + var hostToken = typeof(ProviderClassifier).Assembly.GetName().GetPublicKeyToken(); + if (hostToken is { Length: > 0 }) + { + tokenMatchesHost = rawToken.SequenceEqual(hostToken); + } + } + } + catch (BadImageFormatException ex) + { + return new ProviderClassificationResult + { + Classification = ProviderClassification.Invalid, + Sha256 = sha256, + Diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' is not a valid managed assembly: {ex.Message}", + }; + } + catch (Exception ex) + { + Debug.WriteLine($"[ProviderClassifier] Failed to read strong name for {assemblyPath}: {ex.Message}"); + } + + string? authSubject = null; + string? authThumb = null; + bool authMatchesHost = false; + bool authSignaturePresent = false; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + try + { + var cert = new X509Certificate2(X509Certificate.CreateFromSignedFile(assemblyPath)); + authSubject = cert.Subject; + authThumb = cert.Thumbprint; + authSignaturePresent = true; + + try + { + var hostCert = new X509Certificate2(X509Certificate.CreateFromSignedFile( + Assembly.GetExecutingAssembly().Location)); + authMatchesHost = string.Equals(cert.Thumbprint, hostCert.Thumbprint, StringComparison.OrdinalIgnoreCase); + } + catch (Exception) + { + // Host not signed (e.g. local dev build); cannot compare. + authMatchesHost = false; + } + } + catch (CryptographicException) + { + // No Authenticode signature; treat as unsigned. + } + catch (Exception ex) + { + Debug.WriteLine($"[ProviderClassifier] Authenticode read error for {assemblyPath}: {ex.Message}"); + } + } + + // Official hash manifest lookup (cross-platform). + var hashStatus = ProviderVerificationStatus.NotFound; + try + { + var platform = VersionHelper.GetPlatform(); + var version = VersionHelper.GetDisplayVersion(); + var hashResult = await ProviderHashVerifier.VerifyProviderAsync(assemblyPath, version, platform).ConfigureAwait(false); + hashStatus = hashResult.Status; + } + catch (Exception ex) + { + Debug.WriteLine($"[ProviderClassifier] Hash manifest lookup failed for {assemblyPath}: {ex.Message}"); + } + + // Tampered: any positive signal of "official" combined with a negative crypto signal. + bool hashOnAllowList = hashStatus == ProviderVerificationStatus.Match; + bool hashMismatch = hashStatus == ProviderVerificationStatus.Mismatch; + + if (hashOnAllowList && token is not null && !tokenMatchesHost) + { + // Hash claims official but strong-name token disagrees — tampered. + return new ProviderClassificationResult + { + Classification = ProviderClassification.OfficialTampered, + Sha256 = sha256, + StrongNameToken = token, + StrongNameMatchesHost = tokenMatchesHost, + AuthenticodeSubject = authSubject, + AuthenticodeThumbprint = authThumb, + AuthenticodeMatchesHost = authMatchesHost, + HashStatus = hashStatus, + Diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' is listed in the official hash manifest but its strong-name token does not match SmartHopper. The file has been tampered with.", + }; + } + + if (hashMismatch && tokenMatchesHost) + { + // Strong-name matches SmartHopper but hash mismatches — tampered official build. + return new ProviderClassificationResult + { + Classification = ProviderClassification.OfficialTampered, + Sha256 = sha256, + StrongNameToken = token, + StrongNameMatchesHost = tokenMatchesHost, + AuthenticodeSubject = authSubject, + AuthenticodeThumbprint = authThumb, + AuthenticodeMatchesHost = authMatchesHost, + HashStatus = hashStatus, + Diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' is strong-named by SmartHopper but its SHA-256 hash does not match the published value. The file has been tampered with.", + }; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + && authSignaturePresent && !authMatchesHost && tokenMatchesHost) + { + // Authenticode says someone else signed it, but the strong-name token claims SmartHopper. + return new ProviderClassificationResult + { + Classification = ProviderClassification.OfficialTampered, + Sha256 = sha256, + StrongNameToken = token, + StrongNameMatchesHost = tokenMatchesHost, + AuthenticodeSubject = authSubject, + AuthenticodeThumbprint = authThumb, + AuthenticodeMatchesHost = authMatchesHost, + HashStatus = hashStatus, + Diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' carries SmartHopper's strong-name token but its Authenticode certificate does not match. The file has been tampered with.", + }; + } + + // Official: any positive crypto signal, with no contradicting signal. + bool authOfficialOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && authMatchesHost; + if (tokenMatchesHost || authOfficialOnWindows || hashOnAllowList) + { + return new ProviderClassificationResult + { + Classification = ProviderClassification.Official, + Sha256 = sha256, + StrongNameToken = token, + StrongNameMatchesHost = tokenMatchesHost, + AuthenticodeSubject = authSubject, + AuthenticodeThumbprint = authThumb, + AuthenticodeMatchesHost = authMatchesHost, + HashStatus = hashStatus, + Diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' classified as Official.", + }; + } + + // Community: not an official signal, but still a valid managed assembly. + return new ProviderClassificationResult + { + Classification = ProviderClassification.Community, + Sha256 = sha256, + StrongNameToken = token, + StrongNameMatchesHost = tokenMatchesHost, + AuthenticodeSubject = authSubject, + AuthenticodeThumbprint = authThumb, + AuthenticodeMatchesHost = authMatchesHost, + HashStatus = hashStatus, + Diagnostic = $"Provider '{Path.GetFileName(assemblyPath)}' classified as Community (not signed by SmartHopper).", + }; + } + } +} diff --git a/src/SmartHopper.Infrastructure/AIProviders/ProviderHashVerifier.cs b/src/SmartHopper.Infrastructure/AIProviders/ProviderHashVerifier.cs index 53e7c4521..e3873fa29 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/ProviderHashVerifier.cs +++ b/src/SmartHopper.Infrastructure/AIProviders/ProviderHashVerifier.cs @@ -28,7 +28,8 @@ using System.Threading.Tasks; using Newtonsoft.Json; using SmartHopper.Infrastructure.Utils; - +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Infrastructure.AIProviders { /// @@ -49,30 +50,6 @@ public enum ProviderVerificationStatus NotFound } - /// - /// Provider integrity check mode defining how verification failures are handled. - /// - public enum ProviderIntegrityCheckMode - { - /// - /// Strict mode: Will block providers on mismatch, unavailable and not found. - /// Highest security, requires all providers to have valid published hashes. - /// - Strict, - - /// - /// Hard check mode: Will block providers on mismatch and not found. - /// Allows loading when hash repository is unavailable (network issues). - /// - Hard, - - /// - /// Soft check mode: Will warn but not block providers on mismatch, unavailable and not found. - /// Best for development and third-party providers. Default mode. - /// - Soft - } - /// /// Verification result containing hash comparison status and details. /// diff --git a/src/SmartHopper.Infrastructure/AIProviders/ProviderManager.cs b/src/SmartHopper.Infrastructure/AIProviders/ProviderManager.cs index 50ec81be7..6bbeed2df 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/ProviderManager.cs +++ b/src/SmartHopper.Infrastructure/AIProviders/ProviderManager.cs @@ -33,7 +33,10 @@ using SmartHopper.Infrastructure.Dialogs; using SmartHopper.Infrastructure.Settings; using SmartHopper.Infrastructure.Utils; - +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Infrastructure.AIProviders { /// @@ -52,6 +55,8 @@ public class ProviderManager private readonly ConcurrentDictionary _mismatchedProviders = new ConcurrentDictionary(); // Tracks providers with hash mismatches private readonly ConcurrentDictionary _unavailableProviders = new ConcurrentDictionary(); // Tracks providers where hash check was unavailable (network issues) private readonly ConcurrentDictionary _unknownProviders = new ConcurrentDictionary(); // Tracks providers not found in hash manifest (custom/third-party) + private readonly ConcurrentDictionary _officialProviders = new ConcurrentDictionary(); // Tracks providers classified as official + private readonly ConcurrentDictionary _unsignedProviders = new ConcurrentDictionary(); // Tracks providers loaded without a strong-name signature private ProviderManager() { @@ -193,12 +198,13 @@ await Task.Run(() => RhinoApp.InvokeOnUiThread(() => // SHA-256 hash verification (cross-platform) // In DEBUG builds, force soft check mode to allow local development // In RELEASE builds, use the configured integrity check mode + ProviderVerificationResult? hashResult = null; try { string platform = VersionHelper.GetPlatform(); string version = VersionHelper.GetDisplayVersion(); - var hashResult = await ProviderHashVerifier.VerifyProviderAsync(assemblyPath, version, platform) + hashResult = await ProviderHashVerifier.VerifyProviderAsync(assemblyPath, version, platform) .ConfigureAwait(false); var effectiveMode = SmartHopperSettings.Instance.EffectiveProviderIntegrityCheckMode; @@ -379,6 +385,24 @@ await Task.Run(() => RhinoApp.InvokeOnUiThread(() => // Load the assembly var assembly = Assembly.LoadFrom(assemblyPath); + var loadedAsmName = assembly.GetName().Name ?? Path.GetFileNameWithoutExtension(assemblyPath); + + // Track official/unsigned state for trust reporting + var providerToken = assembly.GetName().GetPublicKeyToken(); + var hostToken = typeof(ProviderManager).Assembly.GetName().GetPublicKeyToken(); + bool tokenMatchesHost = providerToken is { Length: > 0 } && hostToken is { Length: > 0 } && providerToken.SequenceEqual(hostToken); + bool isOfficial = (hashResult?.Status == ProviderVerificationStatus.Match) || tokenMatchesHost; + bool isUnsigned = providerToken is null || providerToken.Length == 0; + + if (isOfficial) + { + this._officialProviders[loadedAsmName] = true; + } + + if (isUnsigned) + { + this._unsignedProviders[loadedAsmName] = true; + } // Find all types that implement IAIProviderFactory var factoryTypes = assembly.GetTypes() @@ -669,6 +693,50 @@ public bool IsProviderUnknown(string providerName) return false; } + /// + /// Checks if a provider is classified as community / non-official. + /// + /// The name of the provider to check. + /// True if the provider is valid but not cryptographically attributable to SmartHopper. + public bool IsProviderCommunity(string providerName) + { + if (string.IsNullOrEmpty(providerName)) + { + return false; + } + + if (this._providerAssemblies.TryGetValue(providerName, out var assembly)) + { + var asmName = assembly.GetName().Name; + return !this._officialProviders.ContainsKey(asmName) + && !this._mismatchedProviders.ContainsKey(asmName) + && !this.IsProviderUnknown(providerName); + } + + return false; + } + + /// + /// Checks if a provider assembly was loaded without a strong-name signature. + /// + /// The name of the provider to check. + /// True if the provider assembly has no public key token. + public bool IsProviderUnsigned(string providerName) + { + if (string.IsNullOrEmpty(providerName)) + { + return false; + } + + if (this._providerAssemblies.TryGetValue(providerName, out var assembly)) + { + var asmName = assembly.GetName().Name; + return this._unsignedProviders.ContainsKey(asmName); + } + + return false; + } + /// /// Gets the default AI provider from settings, or the first available provider if not set. /// diff --git a/src/SmartHopper.Infrastructure/AIProviders/SmartHopperProviderTrustHost.cs b/src/SmartHopper.Infrastructure/AIProviders/SmartHopperProviderTrustHost.cs new file mode 100644 index 000000000..09db6ce42 --- /dev/null +++ b/src/SmartHopper.Infrastructure/AIProviders/SmartHopperProviderTrustHost.cs @@ -0,0 +1,56 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; +namespace SmartHopper.Infrastructure.AIProviders +{ + /// + /// Host-side adapter wiring and + /// into the SDK's + /// abstraction. + /// + public sealed class SmartHopperProviderTrustHost : IProviderTrustHost + { + /// + public ProviderIntegrityCheckMode EffectiveIntegrityCheckMode + => SmartHopperSettings.Instance.EffectiveProviderIntegrityCheckMode; + + /// + public bool IsProviderMismatched(string providerName) + => ProviderManager.Instance.IsProviderMismatched(providerName); + + /// + public bool IsProviderUnavailable(string providerName) + => ProviderManager.Instance.IsProviderUnavailable(providerName); + + /// + public bool IsProviderUnknown(string providerName) + => ProviderManager.Instance.IsProviderUnknown(providerName); + + /// + public bool IsProviderCommunity(string providerName) + => ProviderManager.Instance.IsProviderCommunity(providerName); + + /// + public bool IsProviderUnsigned(string providerName) + => ProviderManager.Instance.IsProviderUnsigned(providerName); + } +} \ No newline at end of file diff --git a/src/SmartHopper.Infrastructure/AITools/AITool.cs b/src/SmartHopper.Infrastructure/AITools/AITool.cs index ed20519a2..0b9c3829c 100644 --- a/src/SmartHopper.Infrastructure/AITools/AITool.cs +++ b/src/SmartHopper.Infrastructure/AITools/AITool.cs @@ -19,11 +19,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AIModels; - +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; namespace SmartHopper.Infrastructure.AITools { /// diff --git a/src/SmartHopper.Infrastructure/AITools/IAIToolProvider.cs b/src/SmartHopper.Infrastructure/AITools/IAIToolProvider.cs index c6aaba01b..b0c16737c 100644 --- a/src/SmartHopper.Infrastructure/AITools/IAIToolProvider.cs +++ b/src/SmartHopper.Infrastructure/AITools/IAIToolProvider.cs @@ -17,7 +17,6 @@ */ using System.Collections.Generic; - namespace SmartHopper.Infrastructure.AITools { /// diff --git a/src/SmartHopper.Infrastructure/AITools/ToolManager.cs b/src/SmartHopper.Infrastructure/AITools/ToolManager.cs index 3203d6bab..acd2b8af6 100644 --- a/src/SmartHopper.Infrastructure/AITools/ToolManager.cs +++ b/src/SmartHopper.Infrastructure/AITools/ToolManager.cs @@ -22,11 +22,10 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.AITools { /// @@ -162,7 +161,7 @@ public static async Task ExecuteTool(AIToolCall toolCall) try { var results = result?.Body?.Interactions? - .OfType() + .OfType() .ToList(); if (results != null && results.Count > 0) { diff --git a/src/SmartHopper.Infrastructure/Dialogs/ProviderVerificationDialog.cs b/src/SmartHopper.Infrastructure/Dialogs/ProviderVerificationDialog.cs index c51f8967e..ce6351d2a 100644 --- a/src/SmartHopper.Infrastructure/Dialogs/ProviderVerificationDialog.cs +++ b/src/SmartHopper.Infrastructure/Dialogs/ProviderVerificationDialog.cs @@ -25,7 +25,7 @@ using Rhino.UI; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Properties; - +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Infrastructure.Dialogs { /// diff --git a/src/SmartHopper.Infrastructure/Dialogs/StyledMessageDialog.cs b/src/SmartHopper.Infrastructure/Dialogs/StyledMessageDialog.cs index 0440fd1bc..c940e7b4b 100644 --- a/src/SmartHopper.Infrastructure/Dialogs/StyledMessageDialog.cs +++ b/src/SmartHopper.Infrastructure/Dialogs/StyledMessageDialog.cs @@ -29,7 +29,6 @@ using Eto.Forms; using Rhino.UI; using SmartHopper.Infrastructure.Properties; - namespace SmartHopper.Infrastructure.Dialogs { /// diff --git a/src/SmartHopper.Infrastructure/Hosting/ProviderSdkHostAdapters.cs b/src/SmartHopper.Infrastructure/Hosting/ProviderSdkHostAdapters.cs new file mode 100644 index 000000000..ae701cd11 --- /dev/null +++ b/src/SmartHopper.Infrastructure/Hosting/ProviderSdkHostAdapters.cs @@ -0,0 +1,313 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using SmartHopper.Infrastructure.AICall.Policies; +using SmartHopper.Infrastructure.AIContext; +using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.Infrastructure.AITools; +using SmartHopper.Infrastructure.Dialogs; +using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; + +namespace SmartHopper.Infrastructure.Hosting +{ + /// + /// Host-side adapter wiring into the SDK's + /// abstraction. + /// + public sealed class SmartHopperProviderRegistryHost : IProviderRegistryHost + { + /// + public IAIProvider GetProvider(string providerName) + => ProviderManager.Instance.GetProvider(providerName); + + /// + public IAIProviderSettings GetProviderSettings(string providerName) + => ProviderManager.Instance.GetProviderSettings(providerName); + } + + /// + /// Host-side adapter wiring the host's into the SDK's + /// abstraction. + /// + public sealed class SmartHopperPolicyPipelineHost : IPolicyPipelineHost + { + /// + public async Task ApplyRequestPoliciesAsync(AIRequestCall request) + { + if (request == null) + { + return; + } + + await PolicyPipeline.Default.ApplyRequestPoliciesAsync(request).ConfigureAwait(false); + } + + /// + public async Task ApplyResponsePoliciesAsync(AIReturn response) + { + if (response == null) + { + return; + } + + await PolicyPipeline.Default.ApplyResponsePoliciesAsync(response).ConfigureAwait(false); + } + } + + /// + /// Host-side adapter wiring into the SDK's + /// abstraction. + /// + public sealed class SmartHopperContextProviderHost : IContextProviderHost + { + /// + public IDictionary GetCurrentContext(string providerFilter = null) + => AIContextManager.GetCurrentContext(providerFilter); + } + + /// + /// Host-side adapter wiring into the SDK's + /// abstraction. + /// + public sealed class SmartHopperToolRegistryHost : IToolRegistryHost + { + /// + public void DiscoverTools() + => AIToolManager.DiscoverTools(); + + /// + public IReadOnlyDictionary GetTools() + { + var hostTools = AIToolManager.GetTools(); + var result = new Dictionary(hostTools.Count); + foreach (var kvp in hostTools) + { + var tool = kvp.Value; + if (tool == null) + { + continue; + } + + result[kvp.Key] = new ProviderToolDefinition + { + Name = tool.Name, + Description = tool.Description, + Enabled = tool.Enabled, + Category = tool.Category, + ParametersSchema = tool.ParametersSchema, + }; + } + + return result; + } + } + + /// + /// Host-side diagnostics sink routing SDK runtime messages to the SmartHopper UI. + /// + public sealed class SmartHopperProviderDiagnosticsHost : IProviderDiagnostics + { + /// + public void Report(string providerName, SHRuntimeMessage message) + { + if (message == null) + { + return; + } + + if (message.Severity == SHRuntimeMessageSeverity.Error) + { + StyledMessageDialog.ShowError(message.Message, providerName ?? "SmartHopper"); + } + else + { + Debug.WriteLine($"[{providerName}] {message.Message}"); + } + } + } + + /// + /// Host-side logger routing SDK log output to Rhino's command-line window. + /// + public sealed class SmartHopperProviderLogger : IProviderLogger + { + /// + public void Log(ProviderLogLevel level, string providerName, string message) + { + var line = $"[{level}][{providerName}] {message}"; + try + { + Rhino.RhinoApp.WriteLine(line); + } + catch + { + // RhinoApp may not be initialized in headless/test runs. + } + + Debug.WriteLine(line); + } + + /// + public void LogException(string providerName, Exception exception, string context = null) + { + if (exception == null) + { + return; + } + + var prefix = string.IsNullOrEmpty(context) ? string.Empty : context + ": "; + var line = $"[Error][{providerName}] {prefix}{exception.GetType().Name}: {exception.Message}"; + try + { + Rhino.RhinoApp.WriteLine(line); + } + catch + { + // RhinoApp may not be initialized in headless/test runs. + } + + Debug.WriteLine(line); + } + } + + /// + /// Host-side factory that reuses one client per provider + /// while honoring per-request timeout overrides. + /// + public sealed class SmartHopperProviderHttpClientFactory : IProviderHttpClientFactory + { + private static readonly ConcurrentDictionary Clients = new ConcurrentDictionary(); + + /// + public HttpClient CreateClient(string providerName, TimeSpan timeout) + { + var name = string.IsNullOrEmpty(providerName) ? "default" : providerName; + var client = Clients.GetOrAdd(name, _ => + { + var c = new HttpClient(); + try + { + c.DefaultRequestHeaders.Add("User-Agent", $"SmartHopper/{name}"); + } + catch + { + // Header may already exist or be restricted; ignore. + } + + return c; + }); + + if (timeout > TimeSpan.Zero) + { + client.Timeout = timeout; + } + + return client; + } + } + + /// + /// Per-provider settings store backed by . + /// + public sealed class SmartHopperProviderSettingsStore : IProviderSettingsStore + { + /// + /// Initializes a new instance of the class. + /// + /// The provider id this store is scoped to. + public SmartHopperProviderSettingsStore(string providerName) + { + this.ProviderName = string.IsNullOrEmpty(providerName) ? string.Empty : providerName; + } + + /// + public string ProviderName { get; } + + /// + public IDictionary GetAll() + => SmartHopperSettings.Instance.GetProviderSettings(this.ProviderName); + + /// + public void SetAll(IDictionary settings) + { + if (settings == null) + { + return; + } + + foreach (var kvp in settings) + { + SmartHopperSettings.Instance.SetSetting(this.ProviderName, kvp.Key, kvp.Value); + } + } + + /// + public T Get(string key, T defaultValue = default) + { + var value = SmartHopperSettings.Instance.GetSetting(this.ProviderName, key); + if (value == null) + { + return defaultValue; + } + + if (value is T typed) + { + return typed; + } + + try + { + return (T)Convert.ChangeType(value, typeof(T)); + } + catch + { + return defaultValue; + } + } + + /// + public void Set(string key, T value) + => SmartHopperSettings.Instance.SetSetting(this.ProviderName, key, value); + } + + /// + /// Factory that creates per-provider instances + /// backed by . + /// + public static class SmartHopperProviderSettingsStoreFactory + { + /// + /// Creates a settings store scoped to . + /// + public static IProviderSettingsStore Create(string providerName) + => new SmartHopperProviderSettingsStore(providerName); + } +} diff --git a/src/SmartHopper.Infrastructure/Initialization/SmartHopperInitializer.cs b/src/SmartHopper.Infrastructure/Initialization/SmartHopperInitializer.cs index 25f0efcc2..ffb4cf24c 100644 --- a/src/SmartHopper.Infrastructure/Initialization/SmartHopperInitializer.cs +++ b/src/SmartHopper.Infrastructure/Initialization/SmartHopperInitializer.cs @@ -22,9 +22,13 @@ using System.Threading.Tasks; using Rhino; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.Infrastructure.Hosting; using SmartHopper.Infrastructure.Settings; using SmartHopper.Infrastructure.Utils; - +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Infrastructure.Initialization { /// @@ -57,6 +61,17 @@ public static void Initialize() { Debug.WriteLine("[SmartHopperInitializer] Starting initialization sequence"); + // Wire Provider SDK host adapters so SDK code paths delegate to SmartHopper infrastructure. + ProviderSdkHost.ProviderTrust = new SmartHopperProviderTrustHost(); + ProviderSdkHost.ProviderRegistry = new SmartHopperProviderRegistryHost(); + ProviderSdkHost.PolicyPipeline = new SmartHopperPolicyPipelineHost(); + ProviderSdkHost.ContextProvider = new SmartHopperContextProviderHost(); + ProviderSdkHost.ToolRegistry = new SmartHopperToolRegistryHost(); + ProviderSdkHost.Diagnostics = new SmartHopperProviderDiagnosticsHost(); + ProviderSdkHost.Logger = new SmartHopperProviderLogger(); + ProviderSdkHost.HttpClientFactory = new SmartHopperProviderHttpClientFactory(); + ProviderSdkHost.SettingsStoreFactory = SmartHopperProviderSettingsStoreFactory.Create; + // Step 1: Load settings first (but don't refresh providers yet) var settings = SmartHopperSettings.Instance; Debug.WriteLine("[SmartHopperInitializer] Settings loaded"); diff --git a/src/SmartHopper.Infrastructure/Mcp/AIToolMcpAdapter.cs b/src/SmartHopper.Infrastructure/Mcp/AIToolMcpAdapter.cs index 7418a0a38..174716d9e 100644 --- a/src/SmartHopper.Infrastructure/Mcp/AIToolMcpAdapter.cs +++ b/src/SmartHopper.Infrastructure/Mcp/AIToolMcpAdapter.cs @@ -30,13 +30,12 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; using SmartHopper.Infrastructure.AICall.Tools; using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; - +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Infrastructure.Mcp { /// diff --git a/src/SmartHopper.Infrastructure/Mcp/JsonRpcDispatcher.cs b/src/SmartHopper.Infrastructure/Mcp/JsonRpcDispatcher.cs index f1f8d17be..fbabec4da 100644 --- a/src/SmartHopper.Infrastructure/Mcp/JsonRpcDispatcher.cs +++ b/src/SmartHopper.Infrastructure/Mcp/JsonRpcDispatcher.cs @@ -28,7 +28,6 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; - namespace SmartHopper.Infrastructure.Mcp { /// diff --git a/src/SmartHopper.Infrastructure/Mcp/McpCanvasLockState.cs b/src/SmartHopper.Infrastructure/Mcp/McpCanvasLockState.cs index e042ef7b2..ba33833fb 100644 --- a/src/SmartHopper.Infrastructure/Mcp/McpCanvasLockState.cs +++ b/src/SmartHopper.Infrastructure/Mcp/McpCanvasLockState.cs @@ -18,7 +18,6 @@ using System; using System.Collections.Generic; - namespace SmartHopper.Infrastructure.Mcp { /// diff --git a/src/SmartHopper.Infrastructure/Mcp/McpServer.cs b/src/SmartHopper.Infrastructure/Mcp/McpServer.cs index d29be57c3..8e3157a35 100644 --- a/src/SmartHopper.Infrastructure/Mcp/McpServer.cs +++ b/src/SmartHopper.Infrastructure/Mcp/McpServer.cs @@ -30,7 +30,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; - namespace SmartHopper.Infrastructure.Mcp { /// diff --git a/src/SmartHopper.Infrastructure/Mcp/McpServerLifecycle.cs b/src/SmartHopper.Infrastructure/Mcp/McpServerLifecycle.cs index ed6ec957a..74b72ae92 100644 --- a/src/SmartHopper.Infrastructure/Mcp/McpServerLifecycle.cs +++ b/src/SmartHopper.Infrastructure/Mcp/McpServerLifecycle.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; - namespace SmartHopper.Infrastructure.Mcp { /// diff --git a/src/SmartHopper.Infrastructure/Mcp/McpServerOptions.cs b/src/SmartHopper.Infrastructure/Mcp/McpServerOptions.cs index d7fffc6a8..644c5b93e 100644 --- a/src/SmartHopper.Infrastructure/Mcp/McpServerOptions.cs +++ b/src/SmartHopper.Infrastructure/Mcp/McpServerOptions.cs @@ -17,7 +17,6 @@ */ using System.Collections.Generic; - namespace SmartHopper.Infrastructure.Mcp { /// diff --git a/src/SmartHopper.Infrastructure/Mcp/McpToolCallResult.cs b/src/SmartHopper.Infrastructure/Mcp/McpToolCallResult.cs index b39e09c31..e28a1c65c 100644 --- a/src/SmartHopper.Infrastructure/Mcp/McpToolCallResult.cs +++ b/src/SmartHopper.Infrastructure/Mcp/McpToolCallResult.cs @@ -17,7 +17,6 @@ */ using Newtonsoft.Json.Linq; - namespace SmartHopper.Infrastructure.Mcp { /// diff --git a/src/SmartHopper.Infrastructure/Mcp/McpToolDescriptor.cs b/src/SmartHopper.Infrastructure/Mcp/McpToolDescriptor.cs index aaf235b6d..19b4b0d0a 100644 --- a/src/SmartHopper.Infrastructure/Mcp/McpToolDescriptor.cs +++ b/src/SmartHopper.Infrastructure/Mcp/McpToolDescriptor.cs @@ -20,7 +20,6 @@ using System.Linq; using Newtonsoft.Json.Linq; using SmartHopper.Infrastructure.AITools; - namespace SmartHopper.Infrastructure.Mcp { /// diff --git a/src/SmartHopper.Infrastructure/Properties/providersResources.Designer.cs b/src/SmartHopper.Infrastructure/Properties/providersResources.Designer.cs index ca804df1d..3f67e0047 100644 --- a/src/SmartHopper.Infrastructure/Properties/providersResources.Designer.cs +++ b/src/SmartHopper.Infrastructure/Properties/providersResources.Designer.cs @@ -11,7 +11,6 @@ namespace SmartHopper.Infrastructure.Properties { using System; - /// /// A strongly-typed resource class, for looking up localized strings, etc. /// diff --git a/src/SmartHopper.Infrastructure/Settings/MacKeychainSecureStorage.cs b/src/SmartHopper.Infrastructure/Settings/MacKeychainSecureStorage.cs index 6b5fde22d..23a5f285d 100644 --- a/src/SmartHopper.Infrastructure/Settings/MacKeychainSecureStorage.cs +++ b/src/SmartHopper.Infrastructure/Settings/MacKeychainSecureStorage.cs @@ -20,7 +20,6 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; - namespace SmartHopper.Infrastructure.Settings { /// diff --git a/src/SmartHopper.Infrastructure/Settings/SmartHopperAssistantSettings.cs b/src/SmartHopper.Infrastructure/Settings/SmartHopperAssistantSettings.cs index a2803b311..d3779970a 100644 --- a/src/SmartHopper.Infrastructure/Settings/SmartHopperAssistantSettings.cs +++ b/src/SmartHopper.Infrastructure/Settings/SmartHopperAssistantSettings.cs @@ -17,7 +17,6 @@ */ using Newtonsoft.Json; - namespace SmartHopper.Infrastructure.Settings { /// diff --git a/src/SmartHopper.Infrastructure/Settings/SmartHopperSettings.cs b/src/SmartHopper.Infrastructure/Settings/SmartHopperSettings.cs index 07761bab3..b0596403e 100644 --- a/src/SmartHopper.Infrastructure/Settings/SmartHopperSettings.cs +++ b/src/SmartHopper.Infrastructure/Settings/SmartHopperSettings.cs @@ -26,7 +26,9 @@ using System.Text; using Newtonsoft.Json; using SmartHopper.Infrastructure.AIProviders; - +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Infrastructure.Settings { /// diff --git a/src/SmartHopper.Infrastructure/Settings/TrustedProviderRecord.cs b/src/SmartHopper.Infrastructure/Settings/TrustedProviderRecord.cs new file mode 100644 index 000000000..9c7cc95f0 --- /dev/null +++ b/src/SmartHopper.Infrastructure/Settings/TrustedProviderRecord.cs @@ -0,0 +1,82 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using Newtonsoft.Json; +namespace SmartHopper.Infrastructure.Settings +{ + /// + /// Persisted record describing a per-provider trust decision. Replaces the legacy + /// Dictionary<string, bool> format keyed by assembly file name. + /// + /// + /// Legacy boolean entries continue to be read for backward compatibility: they are + /// promoted to with + /// set to "Unknown" and the original allow/deny + /// state preserved. Classification is filled in on next discovery. + /// + public sealed class TrustedProviderRecord + { + /// + /// The key used to identify this provider entry. Typically the assembly simple + /// name, optionally suffixed by SHA-256 hash for unsigned providers. + /// + [JsonProperty("key")] + public string Key { get; set; } = string.Empty; + + /// + /// Cryptographic classification recorded at decision time. Stored as a string + /// (e.g. "Official", "Community", "Unknown") so future + /// values added in newer SmartHopper versions don't break older settings files. + /// + [JsonProperty("classification")] + public string Classification { get; set; } = "Unknown"; + + /// + /// Whether the user permitted this provider to load. + /// + [JsonProperty("allowed")] + public bool Allowed { get; set; } + + /// + /// When the decision was made. + /// + [JsonProperty("decidedAt")] + public DateTime DecidedAt { get; set; } = DateTime.UtcNow; + + /// + /// SHA-256 hash of the assembly at decision time. For community providers, a + /// later load with a different hash invalidates the trust. + /// + [JsonProperty("hashAtDecision")] + public string? HashAtDecision { get; set; } + + /// + /// Authenticode signer SHA-1 thumbprint at decision time (Windows only). May be + /// null on macOS or for unsigned providers. + /// + [JsonProperty("signerThumbprintAtDecision")] + public string? SignerThumbprintAtDecision { get; set; } + + /// + /// Strong-name public key token at decision time. null for unsigned providers. + /// + [JsonProperty("strongNameTokenAtDecision")] + public string? StrongNameTokenAtDecision { get; set; } + } +} diff --git a/src/SmartHopper.Infrastructure/SmartHopper.Infrastructure.csproj b/src/SmartHopper.Infrastructure/SmartHopper.Infrastructure.csproj index b91409053..2c3a4a702 100644 --- a/src/SmartHopper.Infrastructure/SmartHopper.Infrastructure.csproj +++ b/src/SmartHopper.Infrastructure/SmartHopper.Infrastructure.csproj @@ -30,6 +30,9 @@ $(SolutionDir)bin/$(SolutionVersion)/$(Configuration) + + + @@ -94,13 +97,11 @@ - - - - - - - + diff --git a/src/SmartHopper.Infrastructure/Utils/VersionHelper.cs b/src/SmartHopper.Infrastructure/Utils/VersionHelper.cs index 6cfb1b679..1d6ea139e 100644 --- a/src/SmartHopper.Infrastructure/Utils/VersionHelper.cs +++ b/src/SmartHopper.Infrastructure/Utils/VersionHelper.cs @@ -20,7 +20,6 @@ using System.Diagnostics; using System.Reflection; using System.Text.RegularExpressions; - namespace SmartHopper.Infrastructure.Utils { /// diff --git a/src/SmartHopper.Menu/Dialogs/AboutDialog.cs b/src/SmartHopper.Menu/Dialogs/AboutDialog.cs index 39251890b..049725cde 100644 --- a/src/SmartHopper.Menu/Dialogs/AboutDialog.cs +++ b/src/SmartHopper.Menu/Dialogs/AboutDialog.cs @@ -25,6 +25,7 @@ using SmartHopper.Infrastructure.Dialogs; using SmartHopper.Infrastructure.Properties; using SmartHopper.Infrastructure.Utils; +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Menu.Dialogs { diff --git a/src/SmartHopper.Menu/Dialogs/SettingsDialog.cs b/src/SmartHopper.Menu/Dialogs/SettingsDialog.cs index d9917057e..64ad27912 100644 --- a/src/SmartHopper.Menu/Dialogs/SettingsDialog.cs +++ b/src/SmartHopper.Menu/Dialogs/SettingsDialog.cs @@ -28,6 +28,9 @@ using SmartHopper.Infrastructure.Settings; using SmartHopper.Menu.Dialogs.SettingsTabs; using SmartHopper.Menu.Dialogs.SettingsTabs.Models; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Menu.Dialogs { diff --git a/src/SmartHopper.Menu/Dialogs/SettingsTabs/AssistantSettingsPage.cs b/src/SmartHopper.Menu/Dialogs/SettingsTabs/AssistantSettingsPage.cs index 54b35743c..2a57e811e 100644 --- a/src/SmartHopper.Menu/Dialogs/SettingsTabs/AssistantSettingsPage.cs +++ b/src/SmartHopper.Menu/Dialogs/SettingsTabs/AssistantSettingsPage.cs @@ -20,6 +20,7 @@ using Eto.Forms; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Menu.Dialogs.SettingsTabs.Models; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Menu.Dialogs.SettingsTabs { diff --git a/src/SmartHopper.Menu/Dialogs/SettingsTabs/FallbackSettingsPage.cs b/src/SmartHopper.Menu/Dialogs/SettingsTabs/FallbackSettingsPage.cs index 59a66f50f..d6bf8f351 100644 --- a/src/SmartHopper.Menu/Dialogs/SettingsTabs/FallbackSettingsPage.cs +++ b/src/SmartHopper.Menu/Dialogs/SettingsTabs/FallbackSettingsPage.cs @@ -24,6 +24,8 @@ using SmartHopper.Infrastructure.AICall.Fallback; using SmartHopper.Infrastructure.AIModels; using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Menu.Dialogs.SettingsTabs { diff --git a/src/SmartHopper.Menu/Dialogs/SettingsTabs/GeneralSettingsPage.cs b/src/SmartHopper.Menu/Dialogs/SettingsTabs/GeneralSettingsPage.cs index e4f1a2aa6..eea4018ae 100644 --- a/src/SmartHopper.Menu/Dialogs/SettingsTabs/GeneralSettingsPage.cs +++ b/src/SmartHopper.Menu/Dialogs/SettingsTabs/GeneralSettingsPage.cs @@ -20,6 +20,7 @@ using Eto.Forms; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Menu.Dialogs.SettingsTabs.Models; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Menu.Dialogs.SettingsTabs { diff --git a/src/SmartHopper.Menu/Dialogs/SettingsTabs/GenericProviderSettingsPage.cs b/src/SmartHopper.Menu/Dialogs/SettingsTabs/GenericProviderSettingsPage.cs index 8dd6b7117..3ed78b110 100644 --- a/src/SmartHopper.Menu/Dialogs/SettingsTabs/GenericProviderSettingsPage.cs +++ b/src/SmartHopper.Menu/Dialogs/SettingsTabs/GenericProviderSettingsPage.cs @@ -25,6 +25,8 @@ using Eto.Forms; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Menu.Dialogs.SettingsTabs { diff --git a/src/SmartHopper.Menu/Dialogs/SettingsTabs/ProvidersSettingsPage.cs b/src/SmartHopper.Menu/Dialogs/SettingsTabs/ProvidersSettingsPage.cs index 09c369d82..32fe650a1 100644 --- a/src/SmartHopper.Menu/Dialogs/SettingsTabs/ProvidersSettingsPage.cs +++ b/src/SmartHopper.Menu/Dialogs/SettingsTabs/ProvidersSettingsPage.cs @@ -22,10 +22,13 @@ using System.Linq; using Eto.Drawing; using Eto.Forms; -using SmartHopper.Infrastructure.AICall.Core; using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Settings; using SmartHopper.Menu.Dialogs.SettingsTabs.Models; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Menu.Dialogs.SettingsTabs { diff --git a/src/SmartHopper.Menu/Items/VerifyProvidersMenuItem.cs b/src/SmartHopper.Menu/Items/VerifyProvidersMenuItem.cs index 6dfc49df5..516f9224b 100644 --- a/src/SmartHopper.Menu/Items/VerifyProvidersMenuItem.cs +++ b/src/SmartHopper.Menu/Items/VerifyProvidersMenuItem.cs @@ -24,6 +24,8 @@ using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Infrastructure.Dialogs; using SmartHopper.Infrastructure.Utils; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Menu.Items { diff --git a/src/SmartHopper.Menu/SmartHopperMenu.cs b/src/SmartHopper.Menu/SmartHopperMenu.cs index ad857875d..b22ecdcd2 100644 --- a/src/SmartHopper.Menu/SmartHopperMenu.cs +++ b/src/SmartHopper.Menu/SmartHopperMenu.cs @@ -26,6 +26,7 @@ using SmartHopper.Infrastructure.Initialization; using SmartHopper.Infrastructure.Settings; using SmartHopper.Menu.Items; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Menu { diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Base/AICallBaseEnumTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Base/AICallBaseEnumTests.cs new file mode 100644 index 000000000..e73f819dd --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Base/AICallBaseEnumTests.cs @@ -0,0 +1,186 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Core.Base +{ + using SmartHopper.ProviderSdk.AICall.Core.Base; + using Xunit; + + /// + /// Tests for , and . + /// + [Collection("ProviderSdk")] + public class AICallBaseEnumTests + { +#if NET7_WINDOWS + [Theory(DisplayName = "AICallStatusExtensions_ToString_ReturnsExpected [Windows]")] +#else + [Theory(DisplayName = "AICallStatusExtensions_ToString_ReturnsExpected [Core]")] +#endif + [InlineData(AICallStatus.Idle, "idle")] + [InlineData(AICallStatus.Processing, "processing")] + [InlineData(AICallStatus.Streaming, "streaming")] + [InlineData(AICallStatus.CallingTools, "calling_tools")] + [InlineData(AICallStatus.Finished, "finished")] + public void AICallStatusExtensions_ToString_ReturnsExpected(AICallStatus status, string expected) + { + Assert.Equal(expected, AICallStatusExtensions.ToString(status)); + } + +#if NET7_WINDOWS + [Theory(DisplayName = "AICallStatusExtensions_ToDescription_ReturnsExpected [Windows]")] +#else + [Theory(DisplayName = "AICallStatusExtensions_ToDescription_ReturnsExpected [Core]")] +#endif + [InlineData(AICallStatus.Idle, "Idle")] + [InlineData(AICallStatus.Processing, "Processing")] + [InlineData(AICallStatus.Streaming, "Streaming")] + [InlineData(AICallStatus.CallingTools, "Calling tools")] + [InlineData(AICallStatus.Finished, "Finished")] + public void AICallStatusExtensions_ToDescription_ReturnsExpected(AICallStatus status, string expected) + { + Assert.Equal(expected, AICallStatusExtensions.ToDescription(status)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AICallStatusExtensions_ToString_UnknownFallback [Windows]")] +#else + [Fact(DisplayName = "AICallStatusExtensions_ToString_UnknownFallback [Core]")] +#endif + public void AICallStatusExtensions_ToString_UnknownFallback() + { + var unknown = (AICallStatus)999; + Assert.Equal("unknown", AICallStatusExtensions.ToString(unknown)); + Assert.Equal("Unknown", AICallStatusExtensions.ToDescription(unknown)); + } + +#if NET7_WINDOWS + [Theory(DisplayName = "AICallStatusExtensions_FromString_ReturnsExpected [Windows]")] +#else + [Theory(DisplayName = "AICallStatusExtensions_FromString_ReturnsExpected [Core]")] +#endif + [InlineData("idle", AICallStatus.Idle)] + [InlineData("processing", AICallStatus.Processing)] + [InlineData("streaming", AICallStatus.Streaming)] + [InlineData("calling_tools", AICallStatus.CallingTools)] + [InlineData("finished", AICallStatus.Finished)] + public void AICallStatusExtensions_FromString_ReturnsExpected(string input, AICallStatus expected) + { + Assert.Equal(expected, AICallStatusExtensions.FromString(input)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AICallStatusExtensions_FromString_UnknownFallback [Windows]")] +#else + [Fact(DisplayName = "AICallStatusExtensions_FromString_UnknownFallback [Core]")] +#endif + public void AICallStatusExtensions_FromString_UnknownFallback() + { + Assert.Equal(AICallStatus.Idle, AICallStatusExtensions.FromString("not_a_status")); + } + +#if NET7_WINDOWS + [Theory(DisplayName = "AIAgentExtensions_ToString_ReturnsExpected [Windows]")] +#else + [Theory(DisplayName = "AIAgentExtensions_ToString_ReturnsExpected [Core]")] +#endif + [InlineData(AIAgent.Context, "context")] + [InlineData(AIAgent.System, "system")] + [InlineData(AIAgent.User, "user")] + [InlineData(AIAgent.Assistant, "assistant")] + [InlineData(AIAgent.ToolCall, "tool_call")] + [InlineData(AIAgent.ToolResult, "tool_result")] + [InlineData(AIAgent.Summary, "summary")] + [InlineData(AIAgent.Error, "error")] + [InlineData(AIAgent.Warning, "warning")] + [InlineData(AIAgent.Info, "info")] + [InlineData(AIAgent.Debug, "debug")] + [InlineData(AIAgent.Unknown, "unknown")] + public void AIAgentExtensions_ToString_ReturnsExpected(AIAgent agent, string expected) + { + Assert.Equal(expected, AIAgentExtensions.ToString(agent)); + } + +#if NET7_WINDOWS + [Theory(DisplayName = "AIAgentExtensions_ToDescription_ReturnsExpected [Windows]")] +#else + [Theory(DisplayName = "AIAgentExtensions_ToDescription_ReturnsExpected [Core]")] +#endif + [InlineData(AIAgent.Context, "Context")] + [InlineData(AIAgent.System, "System")] + [InlineData(AIAgent.User, "User")] + [InlineData(AIAgent.Assistant, "Assistant")] + [InlineData(AIAgent.ToolCall, "Tool Call")] + [InlineData(AIAgent.ToolResult, "Tool Result")] + [InlineData(AIAgent.Summary, "Summary")] + [InlineData(AIAgent.Error, "Error")] + [InlineData(AIAgent.Warning, "Warning")] + [InlineData(AIAgent.Info, "Info")] + [InlineData(AIAgent.Debug, "Debug")] + [InlineData(AIAgent.Unknown, "Unknown")] + public void AIAgentExtensions_ToDescription_ReturnsExpected(AIAgent agent, string expected) + { + Assert.Equal(expected, AIAgentExtensions.ToDescription(agent)); + } + +#if NET7_WINDOWS + [Theory(DisplayName = "AIAgentExtensions_FromString_ReturnsExpected [Windows]")] +#else + [Theory(DisplayName = "AIAgentExtensions_FromString_ReturnsExpected [Core]")] +#endif + [InlineData("context", AIAgent.Context)] + [InlineData("system", AIAgent.System)] + [InlineData("developer", AIAgent.System)] + [InlineData("user", AIAgent.User)] + [InlineData("assistant", AIAgent.Assistant)] + [InlineData("tool_call", AIAgent.ToolCall)] + [InlineData("tool_result", AIAgent.ToolResult)] + [InlineData("tool", AIAgent.ToolResult)] + [InlineData("summary", AIAgent.Summary)] + [InlineData("error", AIAgent.Error)] + [InlineData("warning", AIAgent.Warning)] + [InlineData("info", AIAgent.Info)] + [InlineData("debug", AIAgent.Debug)] + [InlineData("unknown", AIAgent.Unknown)] + public void AIAgentExtensions_FromString_ReturnsExpected(string input, AIAgent expected) + { + Assert.Equal(expected, AIAgentExtensions.FromString(input)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AIAgentExtensions_FromString_UnknownFallback [Windows]")] +#else + [Fact(DisplayName = "AIAgentExtensions_FromString_UnknownFallback [Core]")] +#endif + public void AIAgentExtensions_FromString_UnknownFallback() + { + Assert.Equal(AIAgent.Unknown, AIAgentExtensions.FromString("not_an_agent")); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AIRequestKind_Values [Windows]")] +#else + [Fact(DisplayName = "AIRequestKind_Values [Core]")] +#endif + public void AIRequestKind_Values() + { + Assert.Equal(0, (int)AIRequestKind.Generation); + Assert.Equal(1, (int)AIRequestKind.Backoffice); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyBuilderTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyBuilderTests.cs new file mode 100644 index 000000000..041214d22 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyBuilderTests.cs @@ -0,0 +1,800 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Core.Interactions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Newtonsoft.Json.Linq; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.Diagnostics; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AIBodyBuilderTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "Create_Build_ProducesEmptyBody [Windows]")] +#else + [Fact(DisplayName = "Create_Build_ProducesEmptyBody [Core]")] +#endif + public void Create_Build_ProducesEmptyBody() + { + var body = AIBodyBuilder.Create().Build(); + + Assert.Equal(0, body.InteractionsCount); + Assert.Equal("-*", body.ToolFilter); + Assert.Equal("-*", body.ContextFilter); + Assert.Null(body.JsonOutputSchema); + Assert.False(body.RequiresJsonOutput); + Assert.Empty(body.InteractionsNew); + Assert.True(body.AreTurnIdsValid()); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "FromImmutable_PreservesFiltersAndNewMarkers [Windows]")] +#else + [Fact(DisplayName = "FromImmutable_PreservesFiltersAndNewMarkers [Core]")] +#endif + public void FromImmutable_PreservesFiltersAndNewMarkers() + { + var original = AIBodyBuilder.Create() + .WithToolFilter("+gh_*") + .WithContextFilter("+context") + .WithJsonOutputSchema("{}") + .AddUser("hello") + .Build(); + + var body = AIBodyBuilder.FromImmutable(original) + .AddAssistant("reply") + .Build(); + + Assert.Equal("+gh_*", body.ToolFilter); + Assert.Equal("+context", body.ContextFilter); + Assert.Equal("{}", body.JsonOutputSchema); + Assert.True(body.RequiresJsonOutput); + Assert.Equal(2, body.InteractionsCount); + Assert.Equal(new[] { 0, 1 }, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Add_DefaultNewness_MarksAsNew [Windows]")] +#else + [Fact(DisplayName = "Add_DefaultNewness_MarksAsNew [Core]")] +#endif + public void Add_DefaultNewness_MarksAsNew() + { + var body = AIBodyBuilder.Create() + .Add(new AIInteractionText { Agent = AIAgent.User, Content = "x" }) + .Build(); + + Assert.Equal(1, body.InteractionsCount); + Assert.Single(body.InteractionsNew); + Assert.Contains(0, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Add_ExplicitNewness_False_DoesNotMark [Windows]")] +#else + [Fact(DisplayName = "Add_ExplicitNewness_False_DoesNotMark [Core]")] +#endif + public void Add_ExplicitNewness_False_DoesNotMark() + { + var body = AIBodyBuilder.Create() + .Add(new AIInteractionText { Agent = AIAgent.User, Content = "x" }, false) + .Build(); + + Assert.Empty(body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Add_Null_Ignored [Windows]")] +#else + [Fact(DisplayName = "Add_Null_Ignored [Core]")] +#endif + public void Add_Null_Ignored() + { + var body = AIBodyBuilder.Create() + .Add((IAIInteraction)null!) + .Build(); + + Assert.Equal(0, body.InteractionsCount); + Assert.Empty(body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddRange_DefaultNewness_MarksAll [Windows]")] +#else + [Fact(DisplayName = "AddRange_DefaultNewness_MarksAll [Core]")] +#endif + public void AddRange_DefaultNewness_MarksAll() + { + var body = AIBodyBuilder.Create() + .AddRange(new IAIInteraction[] + { + new AIInteractionText { Agent = AIAgent.User, Content = "a" }, + new AIInteractionText { Agent = AIAgent.Assistant, Content = "b" }, + }) + .Build(); + + Assert.Equal(2, body.InteractionsCount); + Assert.Equal(new[] { 0, 1 }, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddRange_Tuples_RespectsPerItemNewness [Windows]")] +#else + [Fact(DisplayName = "AddRange_Tuples_RespectsPerItemNewness [Core]")] +#endif + public void AddRange_Tuples_RespectsPerItemNewness() + { + var body = AIBodyBuilder.Create() + .AddRange(new (IAIInteraction, bool)[] + { + (new AIInteractionText { Agent = AIAgent.User, Content = "a" }, true), + (new AIInteractionText { Agent = AIAgent.Assistant, Content = "b" }, false), + }) + .Build(); + + Assert.Equal(2, body.InteractionsCount); + Assert.Single(body.InteractionsNew); + Assert.Contains(0, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Theory(DisplayName = "AddUser_AddsUserText [Windows]")] +#else + [Theory(DisplayName = "AddUser_AddsUserText [Core]")] +#endif + [InlineData("hello", true)] + [InlineData("hello", false)] + public void AddUser_AddsUserText(string content, bool markAsNew) + { + var body = AIBodyBuilder.Create() + .AddUser(content, markAsNew) + .Build(); + + Assert.Single(body.Interactions); + var text = Assert.IsType(body.Interactions[0]); + Assert.Equal(AIAgent.User, text.Agent); + Assert.Equal(content, text.Content); + + if (markAsNew) + { + Assert.Single(body.InteractionsNew); + Assert.Contains(0, body.InteractionsNew); + } + else + { + Assert.Empty(body.InteractionsNew); + } + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddAssistant_AddsAssistantText [Windows]")] +#else + [Fact(DisplayName = "AddAssistant_AddsAssistantText [Core]")] +#endif + public void AddAssistant_AddsAssistantText() + { + var body = AIBodyBuilder.Create() + .AddAssistant("reply") + .Build(); + + var text = Assert.IsType(body.Interactions[0]); + Assert.Equal(AIAgent.Assistant, text.Agent); + Assert.Equal("reply", text.Content); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddSystem_AddsSystemText [Windows]")] +#else + [Fact(DisplayName = "AddSystem_AddsSystemText [Core]")] +#endif + public void AddSystem_AddsSystemText() + { + var body = AIBodyBuilder.Create() + .AddSystem("system prompt") + .Build(); + + var text = Assert.IsType(body.Interactions[0]); + Assert.Equal(AIAgent.System, text.Agent); + Assert.Equal("system prompt", text.Content); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddText_WithReasoningAndMetrics [Windows]")] +#else + [Fact(DisplayName = "AddText_WithReasoningAndMetrics [Core]")] +#endif + public void AddText_WithReasoningAndMetrics() + { + var metrics = new AIMetrics { InputTokensPrompt = 5 }; + + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "answer", metrics, "reasoning chain") + .Build(); + + var text = Assert.IsType(body.Interactions[0]); + Assert.Equal(AIAgent.Assistant, text.Agent); + Assert.Equal("answer", text.Content); + Assert.Equal("reasoning chain", text.Reasoning); + Assert.Same(metrics, text.Metrics); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddImageRequest_AddsImageGenerationInteraction [Windows]")] +#else + [Fact(DisplayName = "AddImageRequest_AddsImageGenerationInteraction [Core]")] +#endif + public void AddImageRequest_AddsImageGenerationInteraction() + { + var body = AIBodyBuilder.Create() + .AddImageRequest("a red cube", "1024x1024", "hd", "vivid", "1:1") + .Build(); + + var img = Assert.IsType(body.Interactions[0]); + Assert.Equal(AIAgent.User, img.Agent); + Assert.Equal("a red cube", img.OriginalPrompt); + Assert.Equal("1024x1024", img.ImageSize); + Assert.Equal("hd", img.ImageQuality); + Assert.Equal("vivid", img.ImageStyle); + Assert.Equal("1:1", img.AspectRatio); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddImageInput_WithString_AddsVisionInput [Windows]")] +#else + [Fact(DisplayName = "AddImageInput_WithString_AddsVisionInput [Core]")] +#endif + public void AddImageInput_WithString_AddsVisionInput() + { +#pragma warning disable CA2234 + var body = AIBodyBuilder.Create() + .AddImageInput("https://example.com/image.png") + .Build(); +#pragma warning restore CA2234 + + var img = Assert.IsType(body.Interactions[0]); + Assert.Equal(new Uri("https://example.com/image.png"), img.ImageUrl); + Assert.Null(img.ImageData); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddImageInput_WithUri_AddsVisionInput [Windows]")] +#else + [Fact(DisplayName = "AddImageInput_WithUri_AddsVisionInput [Core]")] +#endif + public void AddImageInput_WithUri_AddsVisionInput() + { + var body = AIBodyBuilder.Create() + .AddImageInput(new Uri("https://example.com/image.png")) + .Build(); + + var img = Assert.IsType(body.Interactions[0]); + Assert.Equal(new Uri("https://example.com/image.png"), img.ImageUrl); + Assert.Null(img.ImageData); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddImageInputFromBase64_AddsVisionInput [Windows]")] +#else + [Fact(DisplayName = "AddImageInputFromBase64_AddsVisionInput [Core]")] +#endif + public void AddImageInputFromBase64_AddsVisionInput() + { + var body = AIBodyBuilder.Create() + .AddImageInputFromBase64("abc123", "image/jpeg") + .Build(); + + var img = Assert.IsType(body.Interactions[0]); + Assert.Equal("abc123", img.ImageData); + Assert.Equal("image/jpeg", img.MimeType); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddToolCall_AddsToolCall [Windows]")] +#else + [Fact(DisplayName = "AddToolCall_AddsToolCall [Core]")] +#endif + public void AddToolCall_AddsToolCall() + { + var args = new JObject(new JProperty("x", 1)); + + var body = AIBodyBuilder.Create() + .AddToolCall("call-1", "test_tool", args) + .Build(); + + var tc = Assert.IsType(body.Interactions[0]); + Assert.Equal(AIAgent.ToolCall, tc.Agent); + Assert.Equal("call-1", tc.Id); + Assert.Equal("test_tool", tc.Name); + Assert.Same(args, tc.Arguments); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddToolResult_AddsToolResult [Windows]")] +#else + [Fact(DisplayName = "AddToolResult_AddsToolResult [Core]")] +#endif + public void AddToolResult_AddsToolResult() + { + var result = new JObject(new JProperty("ok", true)); + var messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Info, SHRuntimeMessageOrigin.Provider, SHMessageCode.Unknown, "note"), + }; + var metrics = new AIMetrics { InputTokensPrompt = 10 }; + + var body = AIBodyBuilder.Create() + .AddToolResult(result, "call-1", "test_tool", metrics, messages) + .Build(); + + var tr = Assert.IsType(body.Interactions[0]); + Assert.Equal(AIAgent.ToolResult, tr.Agent); + Assert.Equal("call-1", tr.Id); + Assert.Equal("test_tool", tr.Name); + Assert.Same(result, tr.Result); + Assert.Same(messages, tr.Messages); + Assert.Same(metrics, tr.Metrics); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddError_AddsErrorDiagnostic [Windows]")] +#else + [Fact(DisplayName = "AddError_AddsErrorDiagnostic [Core]")] +#endif + public void AddError_AddsErrorDiagnostic() + { + var body = AIBodyBuilder.Create() + .AddError("boom") + .Build(); + + var diag = Assert.IsType(body.Interactions[0]); + Assert.Equal(SHRuntimeMessageSeverity.Error, diag.Severity); + Assert.Equal("boom", diag.Content); + Assert.True(diag.Surfaceable); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddWarning_AddsWarningDiagnostic [Windows]")] +#else + [Fact(DisplayName = "AddWarning_AddsWarningDiagnostic [Core]")] +#endif + public void AddWarning_AddsWarningDiagnostic() + { + var body = AIBodyBuilder.Create() + .AddWarning("watch out") + .Build(); + + var diag = Assert.IsType(body.Interactions[0]); + Assert.Equal(SHRuntimeMessageSeverity.Warning, diag.Severity); + Assert.Equal("watch out", diag.Content); + Assert.True(diag.Surfaceable); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddInfo_AddsInfoDiagnostic [Windows]")] +#else + [Fact(DisplayName = "AddInfo_AddsInfoDiagnostic [Core]")] +#endif + public void AddInfo_AddsInfoDiagnostic() + { + var body = AIBodyBuilder.Create() + .AddInfo("noted") + .Build(); + + var diag = Assert.IsType(body.Interactions[0]); + Assert.Equal(SHRuntimeMessageSeverity.Info, diag.Severity); + Assert.Equal("noted", diag.Content); + Assert.True(diag.Surfaceable); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddDebug_AddsDebugDiagnosticNonSurfaceable [Windows]")] +#else + [Fact(DisplayName = "AddDebug_AddsDebugDiagnosticNonSurfaceable [Core]")] +#endif + public void AddDebug_AddsDebugDiagnosticNonSurfaceable() + { + var body = AIBodyBuilder.Create() + .AddDebug("trace") + .Build(); + + var diag = Assert.IsType(body.Interactions[0]); + Assert.Equal(SHRuntimeMessageSeverity.Debug, diag.Severity); + Assert.Equal("trace", diag.Content); + Assert.False(diag.Surfaceable); + } + +#if NET7_WINDOWS + [Theory(DisplayName = "Diagnostics_RespectNewness [Windows]")] +#else + [Theory(DisplayName = "Diagnostics_RespectNewness [Core]")] +#endif + [InlineData(true)] + [InlineData(false)] + public void Diagnostics_RespectNewness(bool markAsNew) + { + var body = AIBodyBuilder.Create() + .AsHistory() + .AddError("err", markAsNew) + .Build(); + + if (markAsNew) + { + Assert.Single(body.InteractionsNew); + Assert.Contains(0, body.InteractionsNew); + } + else + { + Assert.Empty(body.InteractionsNew); + } + } + +#if NET7_WINDOWS + [Fact(DisplayName = "WithToolFilter_WithContextFilter_WithJsonOutputSchema_PreservedOnBuild [Windows]")] +#else + [Fact(DisplayName = "WithToolFilter_WithContextFilter_WithJsonOutputSchema_PreservedOnBuild [Core]")] +#endif + public void WithToolFilter_WithContextFilter_WithJsonOutputSchema_PreservedOnBuild() + { + var body = AIBodyBuilder.Create() + .WithToolFilter("+gh_*") + .WithContextFilter("+ctx") + .WithJsonOutputSchema("{\"type\":\"object\"}") + .WithToolFilter(null!) + .WithContextFilter(null!) + .WithJsonOutputSchema(null!) + .AddUser("hi") + .Build(); + + Assert.Equal("+gh_*", body.ToolFilter); + Assert.Equal("+ctx", body.ContextFilter); + Assert.Equal("{\"type\":\"object\"}", body.JsonOutputSchema); + Assert.True(body.RequiresJsonOutput); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "WithTurnId_AssignedToInteractionsWithoutTurnId [Windows]")] +#else + [Fact(DisplayName = "WithTurnId_AssignedToInteractionsWithoutTurnId [Core]")] +#endif + public void WithTurnId_AssignedToInteractionsWithoutTurnId() + { + var body = AIBodyBuilder.Create() + .WithTurnId("turn-shared") + .AddUser("u1") + .AddAssistant("a1") + .Build(); + + Assert.True(body.AreTurnIdsValid()); + Assert.All(body.Interactions, i => Assert.Equal("turn-shared", i.TurnId)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "WithTurnId_PreservesPreExistingTurnId [Windows]")] +#else + [Fact(DisplayName = "WithTurnId_PreservesPreExistingTurnId [Core]")] +#endif + public void WithTurnId_PreservesPreExistingTurnId() + { + var existing = new AIInteractionText { Agent = AIAgent.User, Content = "u", TurnId = "existing" }; + + var body = AIBodyBuilder.Create() + .WithTurnId("default") + .Add(existing) + .Build(); + + Assert.Equal("existing", body.Interactions[0].TurnId); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AsHistory_AsNew_ControlDefaultNewness [Windows]")] +#else + [Fact(DisplayName = "AsHistory_AsNew_ControlDefaultNewness [Core]")] +#endif + public void AsHistory_AsNew_ControlDefaultNewness() + { + var historyBody = AIBodyBuilder.Create() + .AsHistory() + .AddUser("u") + .Build(); + + Assert.Empty(historyBody.InteractionsNew); + + var newBody = AIBodyBuilder.Create() + .AsNew() + .AddUser("u") + .Build(); + + Assert.Single(newBody.InteractionsNew); + Assert.Contains(0, newBody.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ClearNewMarkers_ClearsAndAllowsFurtherMarking [Windows]")] +#else + [Fact(DisplayName = "ClearNewMarkers_ClearsAndAllowsFurtherMarking [Core]")] +#endif + public void ClearNewMarkers_ClearsAndAllowsFurtherMarking() + { + var body = AIBodyBuilder.Create() + .AddUser("u") + .ClearNewMarkers() + .AddAssistant("a") + .Build(); + + Assert.Single(body.InteractionsNew); + Assert.Contains(1, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "MarkLastAsNew_MarksLastInteraction [Windows]")] +#else + [Fact(DisplayName = "MarkLastAsNew_MarksLastInteraction [Core]")] +#endif + public void MarkLastAsNew_MarksLastInteraction() + { + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("u") + .AddAssistant("a") + .MarkLastAsNew() + .Build(); + + Assert.Single(body.InteractionsNew); + Assert.Contains(1, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "MarkLastNAsNew_MarksLastN [Windows]")] +#else + [Fact(DisplayName = "MarkLastNAsNew_MarksLastN [Core]")] +#endif + public void MarkLastNAsNew_MarksLastN() + { + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("u") + .AddAssistant("a") + .AddSystem("s") + .MarkLastNAsNew(2) + .Build(); + + Assert.Equal(new[] { 1, 2 }, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "MarkIndicesAsNew_MarksProvidedIndices [Windows]")] +#else + [Fact(DisplayName = "MarkIndicesAsNew_MarksProvidedIndices [Core]")] +#endif + public void MarkIndicesAsNew_MarksProvidedIndices() + { + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("u") + .AddAssistant("a") + .AddSystem("s") + .MarkIndicesAsNew(new[] { 0, 2 }) + .Build(); + + Assert.Equal(new[] { 0, 2 }, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ReplaceLast_DefaultNewness_ReplacesAndMarks [Windows]")] +#else + [Fact(DisplayName = "ReplaceLast_DefaultNewness_ReplacesAndMarks [Core]")] +#endif + public void ReplaceLast_DefaultNewness_ReplacesAndMarks() + { + var body = AIBodyBuilder.Create() + .AddUser("a") + .AddAssistant("b") + .ReplaceLast(new AIInteractionText { Agent = AIAgent.Assistant, Content = "c" }) + .Build(); + + Assert.Equal(2, body.InteractionsCount); + var last = Assert.IsType(body.Interactions[1]); + Assert.Equal("c", last.Content); + Assert.Equal(new[] { 0, 1 }, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ReplaceLast_ExplicitNewness_False_DoesNotMark [Windows]")] +#else + [Fact(DisplayName = "ReplaceLast_ExplicitNewness_False_DoesNotMark [Core]")] +#endif + public void ReplaceLast_ExplicitNewness_False_DoesNotMark() + { + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("a") + .AddAssistant("b") + .ReplaceLast(new AIInteractionText { Agent = AIAgent.Assistant, Content = "c" }, false) + .Build(); + + Assert.Equal(2, body.InteractionsCount); + Assert.Empty(body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ReplaceLast_WhenEmpty_TreatsAsAdd [Windows]")] +#else + [Fact(DisplayName = "ReplaceLast_WhenEmpty_TreatsAsAdd [Core]")] +#endif + public void ReplaceLast_WhenEmpty_TreatsAsAdd() + { + var body = AIBodyBuilder.Create() + .ReplaceLast(new AIInteractionText { Agent = AIAgent.User, Content = "x" }) + .Build(); + + Assert.Single(body.Interactions); + Assert.Single(body.InteractionsNew); + Assert.Contains(0, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ReplaceLastRange_Slice_WithDefaultNewness [Windows]")] +#else + [Fact(DisplayName = "ReplaceLastRange_Slice_WithDefaultNewness [Core]")] +#endif + public void ReplaceLastRange_Slice_WithDefaultNewness() + { + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("a") + .AddAssistant("b") + .AddSystem("c") + .AsNew() + .ReplaceLastRange(new List + { + new AIInteractionText { Agent = AIAgent.Assistant, Content = "y" }, + new AIInteractionText { Agent = AIAgent.Assistant, Content = "z" }, + }) + .Build(); + + Assert.Equal(3, body.InteractionsCount); + Assert.IsType(body.Interactions[0]); + Assert.Equal("y", ((AIInteractionText)body.Interactions[1]).Content); + Assert.Equal("z", ((AIInteractionText)body.Interactions[2]).Content); + Assert.Equal(new[] { 1, 2 }, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ReplaceLastRange_Slice_ExplicitNewness [Windows]")] +#else + [Fact(DisplayName = "ReplaceLastRange_Slice_ExplicitNewness [Core]")] +#endif + public void ReplaceLastRange_Slice_ExplicitNewness() + { + var replacements = new List + { + new AIInteractionText { Agent = AIAgent.Assistant, Content = "y" }, + new AIInteractionText { Agent = AIAgent.Assistant, Content = "z" }, + }; + + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("a") + .AddAssistant("b") + .AddSystem("c") + .ReplaceLastRange(replacements, false) + .Build(); + + Assert.Equal(3, body.InteractionsCount); + Assert.Empty(body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ReplaceLastRange_ResetWhenReplacingMoreThanExisting [Windows]")] +#else + [Fact(DisplayName = "ReplaceLastRange_ResetWhenReplacingMoreThanExisting [Core]")] +#endif + public void ReplaceLastRange_ResetWhenReplacingMoreThanExisting() + { + var replacements = new List + { + new AIInteractionText { Agent = AIAgent.Assistant, Content = "x" }, + new AIInteractionText { Agent = AIAgent.Assistant, Content = "y" }, + new AIInteractionText { Agent = AIAgent.Assistant, Content = "z" }, + }; + + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("a") + .AddAssistant("b") + .ReplaceLastRange(replacements, true) + .Build(); + + Assert.Equal(3, body.InteractionsCount); + Assert.Equal(new[] { 0, 1, 2 }, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ReplaceLastRange_TupleOverload_RespectsPerItemNewness [Windows]")] +#else + [Fact(DisplayName = "ReplaceLastRange_TupleOverload_RespectsPerItemNewness [Core]")] +#endif + public void ReplaceLastRange_TupleOverload_RespectsPerItemNewness() + { + var replacements = new List<(IAIInteraction, bool)> + { + (new AIInteractionText { Agent = AIAgent.Assistant, Content = "y" }, true), + (new AIInteractionText { Agent = AIAgent.Assistant, Content = "z" }, false), + }; + + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("a") + .AddAssistant("b") + .AddSystem("c") + .ReplaceLastRange(replacements) + .Build(); + + Assert.Equal(3, body.InteractionsCount); + Assert.Single(body.InteractionsNew); + Assert.Contains(1, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ReplaceLastRange_EmptyList_DoesNothing [Windows]")] +#else + [Fact(DisplayName = "ReplaceLastRange_EmptyList_DoesNothing [Core]")] +#endif + public void ReplaceLastRange_EmptyList_DoesNothing() + { + var body = AIBodyBuilder.Create() + .AddUser("u") + .ReplaceLastRange(new List()) + .Build(); + + Assert.Single(body.Interactions); + Assert.Equal("u", ((AIInteractionText)body.Interactions[0]).Content); + Assert.Single(body.InteractionsNew); + Assert.Contains(0, body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Build_ClampsAndDeduplicatesNewMarkers [Windows]")] +#else + [Fact(DisplayName = "Build_ClampsAndDeduplicatesNewMarkers [Core]")] +#endif + public void Build_ClampsAndDeduplicatesNewMarkers() + { + var body = AIBodyBuilder.Create() + .AsHistory() + .AddUser("u") + .MarkLastAsNew() + .MarkLastAsNew() + .MarkIndicesAsNew(new[] { 0, 0, 99 }) + .Build(); + + Assert.Single(body.InteractionsNew); + Assert.Contains(0, body.InteractionsNew); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyTests.cs new file mode 100644 index 000000000..a6ed9b12e --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyTests.cs @@ -0,0 +1,135 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Core.Interactions +{ + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.Tests.TestHelpers; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AIBodyTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "Metrics_CombinesInteractions [Windows]")] +#else + [Fact(DisplayName = "Metrics_CombinesInteractions [Core]")] +#endif + public void Metrics_CombinesInteractions() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + + var first = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 100, + OutputTokensGeneration = 50, + }; + + var second = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 50, + OutputTokensGeneration = 25, + }; + + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "first", first) + .AddText(AIAgent.Assistant, "second", second) + .Build(); + + var metrics = body.Metrics; + + Assert.Equal(150, metrics.InputTokensPrompt); + Assert.Equal(75, metrics.OutputTokensGeneration); + Assert.Equal(225, metrics.TotalTokens); + Assert.Equal("openai", metrics.Provider); + Assert.Equal("gpt-test", metrics.Model); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Metrics_CombinesEstimatedCost [Windows]")] +#else + [Fact(DisplayName = "Metrics_CombinesEstimatedCost [Core]")] +#endif + public void Metrics_CombinesEstimatedCost() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + AIModelCapabilityRegistry.Instance.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-test", + Pricing = new AIModelPricing { Prompt = 0.000001m, Completion = 0.000004m }, + }); + + var first = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 100, + OutputTokensGeneration = 50, + }; + + var second = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 50, + OutputTokensGeneration = 25, + }; + + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "first", first) + .AddText(AIAgent.Assistant, "second", second) + .Build(); + + var expected = (150m * 0.000001m) + (75m * 0.000004m); + Assert.Equal(expected, body.Metrics.EstimatedCost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "GetEffectiveTokenCount_UsesMaxOfActualAndEstimated [Windows]")] +#else + [Fact(DisplayName = "GetEffectiveTokenCount_UsesMaxOfActualAndEstimated [Core]")] +#endif + public void GetEffectiveTokenCount_UsesMaxOfActualAndEstimated() + { + var metrics = new AIMetrics + { + InputTokensPrompt = 100, + OutputTokensGeneration = 50, + EstimatedInputTokens = 500, + EstimatedOutputTokens = 100, + }; + + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "test", metrics) + .Build(); + + Assert.Equal(600, body.GetEffectiveTokenCount()); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyValidationTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyValidationTests.cs new file mode 100644 index 000000000..80469b0d9 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIBodyValidationTests.cs @@ -0,0 +1,344 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Core.Interactions +{ + using System.Collections.Generic; + using Newtonsoft.Json.Linq; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.Diagnostics; + using Xunit; + + /// + /// Tests for validation, messages and metrics aggregation. + /// + [Collection("ProviderSdk")] + public class AIBodyValidationTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "Empty_Body_HasExpectedDefaults [Windows]")] +#else + [Fact(DisplayName = "Empty_Body_HasExpectedDefaults [Core]")] +#endif + public void Empty_Body_HasExpectedDefaults() + { + var body = AIBody.Empty; + + Assert.Equal(0, body.InteractionsCount); + Assert.Equal("-*", body.ToolFilter); + Assert.Equal("-*", body.ContextFilter); + Assert.Null(body.JsonOutputSchema); + Assert.False(body.RequiresJsonOutput); + Assert.Empty(body.InteractionsNew); + Assert.True(body.AreTurnIdsValid()); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "InteractionsCount_ReflectsInteractionCount [Windows]")] +#else + [Fact(DisplayName = "InteractionsCount_ReflectsInteractionCount [Core]")] +#endif + public void InteractionsCount_ReflectsInteractionCount() + { + var body = AIBodyBuilder.Create() + .AddUser("u") + .AddAssistant("a") + .Build(); + + Assert.Equal(2, body.InteractionsCount); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "RequiresJsonOutput_TrueWhenSchemaPresent [Windows]")] +#else + [Fact(DisplayName = "RequiresJsonOutput_TrueWhenSchemaPresent [Core]")] +#endif + public void RequiresJsonOutput_TrueWhenSchemaPresent() + { + var withSchema = AIBodyBuilder.Create() + .WithJsonOutputSchema("{\"type\":\"object\"}") + .Build(); + + Assert.True(withSchema.RequiresJsonOutput); + + var withoutSchema = AIBodyBuilder.Create().Build(); + Assert.False(withoutSchema.RequiresJsonOutput); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AreTurnIdsValid_True_WhenAllHaveTurnId [Windows]")] +#else + [Fact(DisplayName = "AreTurnIdsValid_True_WhenAllHaveTurnId [Core]")] +#endif + public void AreTurnIdsValid_True_WhenAllHaveTurnId() + { + var body = AIBodyBuilder.Create() + .AddUser("u") + .AddAssistant("a") + .Build(); + + Assert.True(body.AreTurnIdsValid()); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AreTurnIdsValid_False_WhenMissingTurnId [Windows]")] +#else + [Fact(DisplayName = "AreTurnIdsValid_False_WhenMissingTurnId [Core]")] +#endif + public void AreTurnIdsValid_False_WhenMissingTurnId() + { + var text = new AIInteractionText + { + Agent = AIAgent.User, + Content = "u", + TurnId = null!, + }; + + var body = new AIBody( + new List { text }, + "-*", + "-*", + null!, + new List()); + + Assert.False(body.AreTurnIdsValid()); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ResetNew_ClearsNewMarkers [Windows]")] +#else + [Fact(DisplayName = "ResetNew_ClearsNewMarkers [Core]")] +#endif + public void ResetNew_ClearsNewMarkers() + { + var body = AIBodyBuilder.Create() + .AddUser("u") + .Build(); + + Assert.Single(body.InteractionsNew); + + body.ResetNew(); + + Assert.Empty(body.InteractionsNew); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Messages_AggregatesFromToolResultImageAndRuntimeMessage [Windows]")] +#else + [Fact(DisplayName = "Messages_AggregatesFromToolResultImageAndRuntimeMessage [Core]")] +#endif + public void Messages_AggregatesFromToolResultImageAndRuntimeMessage() + { + var toolResult = new AIInteractionToolResult + { + Id = "call-1", + Name = "test_tool", + Result = new JObject(new JProperty("ok", true)), + Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Info, SHRuntimeMessageOrigin.Provider, SHMessageCode.Unknown, "tool message"), + }, + }; + + var image = new AIInteractionImage + { + Agent = AIAgent.User, + OriginalPrompt = "a red cube", + Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Warning, SHRuntimeMessageOrigin.Provider, SHMessageCode.Unknown, "image message"), + }, + }; + + var runtime = new AIInteractionRuntimeMessage + { + Severity = SHRuntimeMessageSeverity.Info, + Content = "runtime message", + }; + + var body = new AIBody( + new List { toolResult, image, runtime }, + "-*", + "-*", + null!, + new List()); + + var messages = body.Messages; + + Assert.Equal(3, messages.Count); + Assert.Contains(messages, m => m.Message == "tool message"); + Assert.Contains(messages, m => m.Message == "image message"); + Assert.Contains(messages, m => m.Message == "runtime message"); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Messages_DeduplicatesByMessageText [Windows]")] +#else + [Fact(DisplayName = "Messages_DeduplicatesByMessageText [Core]")] +#endif + public void Messages_DeduplicatesByMessageText() + { + var toolResult = new AIInteractionToolResult + { + Result = new JObject(), + Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Info, SHRuntimeMessageOrigin.Provider, SHMessageCode.Unknown, "duplicate"), + }, + }; + + var image = new AIInteractionImage + { + Agent = AIAgent.User, + Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Warning, SHRuntimeMessageOrigin.Provider, SHMessageCode.Unknown, "duplicate"), + }, + }; + + var runtime = new AIInteractionRuntimeMessage + { + Severity = SHRuntimeMessageSeverity.Info, + Content = "duplicate", + }; + + var body = new AIBody( + new List { toolResult, image, runtime }, + "-*", + "-*", + null!, + new List()); + + var messages = body.Messages; + + Assert.Single(messages); + Assert.Equal("duplicate", messages[0].Message); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Metrics_AggregatesAcrossInteractions [Windows]")] +#else + [Fact(DisplayName = "Metrics_AggregatesAcrossInteractions [Core]")] +#endif + public void Metrics_AggregatesAcrossInteractions() + { + var toolResult = new AIInteractionToolResult + { + Result = new JObject(), + Metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 10, + OutputTokensGeneration = 5, + }, + }; + + var image = new AIInteractionImage + { + Agent = AIAgent.User, + Metrics = new AIMetrics + { + InputTokensPrompt = 5, + OutputTokensReasoning = 2, + }, + }; + + var text = new AIInteractionText + { + Agent = AIAgent.User, + Content = "u", + Metrics = new AIMetrics + { + InputTokensCached = 3, + }, + }; + + var body = new AIBody( + new List { toolResult, image, text }, + "-*", + "-*", + null!, + new List()); + + Assert.Equal(15, body.Metrics.InputTokensPrompt); + Assert.Equal(3, body.Metrics.InputTokensCached); + Assert.Equal(18, body.Metrics.InputTokens); + Assert.Equal(5, body.Metrics.OutputTokensGeneration); + Assert.Equal(2, body.Metrics.OutputTokensReasoning); + Assert.Equal(7, body.Metrics.OutputTokens); + Assert.Equal(25, body.Metrics.TotalTokens); + Assert.Equal("openai", body.Metrics.Provider); + Assert.Equal("gpt-test", body.Metrics.Model); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Metrics_IterationsCount_DefaultsToOne [Windows]")] +#else + [Fact(DisplayName = "Metrics_IterationsCount_DefaultsToOne [Core]")] +#endif + public void Metrics_IterationsCount_DefaultsToOne() + { + var body = new AIBody( + new List + { + new AIInteractionText { Agent = AIAgent.User, Content = "u" }, + new AIInteractionText { Agent = AIAgent.Assistant, Content = "a" }, + }, + "-*", + "-*", + null!, + new List()); + + Assert.Equal(1, body.Metrics.IterationsCount); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Metrics_IterationsCount_AggregatesExplicitValues [Windows]")] +#else + [Fact(DisplayName = "Metrics_IterationsCount_AggregatesExplicitValues [Core]")] +#endif + public void Metrics_IterationsCount_AggregatesExplicitValues() + { + var first = new AIInteractionText + { + Agent = AIAgent.User, + Content = "u", + Metrics = new AIMetrics { IterationsCount = 2 }, + }; + + var second = new AIInteractionText + { + Agent = AIAgent.Assistant, + Content = "a", + Metrics = new AIMetrics { IterationsCount = 3 }, + }; + + var body = new AIBody( + new List { first, second }, + "-*", + "-*", + null!, + new List()); + + Assert.Equal(5, body.Metrics.IterationsCount); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIInteractionTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIInteractionTests.cs new file mode 100644 index 000000000..a79452364 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Interactions/AIInteractionTests.cs @@ -0,0 +1,845 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Core.Interactions +{ + using System; + using System.Collections.Generic; + using System.IO; + using Newtonsoft.Json.Linq; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.AICall.Utilities; + using SmartHopper.ProviderSdk.Diagnostics; + using Xunit; + + /// + /// Tests for the , , + /// , , + /// and interactions. + /// + [Collection("ProviderSdk")] + public class AIInteractionTests + { +#if NET7_WINDOWS + private const string PlatformSuffix = " [Windows]"; +#else + private const string PlatformSuffix = " [Core]"; +#endif + + #region AIInteractionText + + [Fact(DisplayName = nameof(Text_SetResult) + PlatformSuffix)] + public void Text_SetResult() + { + var interaction = new AIInteractionText(); + interaction.SetResult(AIAgent.Assistant, "content", "reasoning"); + + Assert.Equal(AIAgent.Assistant, interaction.Agent); + Assert.Equal("content", interaction.Content); + Assert.Equal("reasoning", interaction.Reasoning); + } + + [Fact(DisplayName = nameof(Text_AppendDeltaCombinesContentReasoningAndMetrics) + PlatformSuffix)] + public void Text_AppendDeltaCombinesContentReasoningAndMetrics() + { + var interaction = new AIInteractionText(); + interaction.SetResult(AIAgent.Assistant, "Hello", "think"); + + var metrics = new AIMetrics + { + InputTokensPrompt = 10, + OutputTokensGeneration = 5, + }; + + interaction.AppendDelta( + contentDelta: " world", + reasoningDelta: " more", + metricsDelta: metrics); + + Assert.Equal("Hello world", interaction.Content); + Assert.Equal("think more", interaction.Reasoning); + Assert.Equal(10, interaction.Metrics.InputTokensPrompt); + Assert.Equal(5, interaction.Metrics.OutputTokensGeneration); + Assert.Equal(15, interaction.Metrics.TotalTokens); + } + + [Fact(DisplayName = nameof(Text_ToStringWithoutReasoning) + PlatformSuffix)] + public void Text_ToStringWithoutReasoning() + { + var interaction = new AIInteractionText(); + interaction.SetResult(AIAgent.Assistant, "only content"); + + Assert.Equal("only content", interaction.ToString()); + } + + [Fact(DisplayName = nameof(Text_ToStringWithReasoning) + PlatformSuffix)] + public void Text_ToStringWithReasoning() + { + var interaction = new AIInteractionText(); + interaction.SetResult(AIAgent.Assistant, "answer", "thinking"); + + var result = interaction.ToString(); + + Assert.Contains("", result); + Assert.Contains("", result); + Assert.Contains("thinking", result); + Assert.Contains("answer", result); + } + + [Fact(DisplayName = nameof(Text_GetStreamKeyWithoutTurnId) + PlatformSuffix)] + public void Text_GetStreamKeyWithoutTurnId() + { + var interaction = new AIInteractionText(); + interaction.SetResult(AIAgent.Assistant, "content"); + + Assert.Equal("text:assistant", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(Text_GetStreamKeyWithTurnId) + PlatformSuffix)] + public void Text_GetStreamKeyWithTurnId() + { + var interaction = new AIInteractionText + { + TurnId = "turn-1", + }; + interaction.SetResult(AIAgent.User, "content"); + + Assert.Equal("turn:turn-1:user", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(Text_GetDedupKey) + PlatformSuffix)] + public void Text_GetDedupKey() + { + var interaction = new AIInteractionText + { + TurnId = "turn-1", + }; + interaction.SetResult(AIAgent.Assistant, "content"); + + var hash = HashUtility.ComputeShortHash("turn-1:assistant:content"); + var expected = $"turn:turn-1:assistant:{hash}"; + + Assert.Equal(expected, interaction.GetDedupKey()); + } + + [Fact(DisplayName = nameof(Text_GetRoleClassForRender) + PlatformSuffix)] + public void Text_GetRoleClassForRender() + { + var assistant = new AIInteractionText(); + assistant.SetResult(AIAgent.Assistant, "a"); + + var user = new AIInteractionText(); + user.SetResult(AIAgent.User, "b"); + + Assert.Equal("assistant", assistant.GetRoleClassForRender()); + Assert.Equal("user", user.GetRoleClassForRender()); + } + + [Fact(DisplayName = nameof(Text_GetDisplayNameForRender) + PlatformSuffix)] + public void Text_GetDisplayNameForRender() + { + var assistant = new AIInteractionText(); + assistant.SetResult(AIAgent.Assistant, "a"); + + Assert.Equal("Assistant", assistant.GetDisplayNameForRender()); + } + + [Fact(DisplayName = nameof(Text_GetRawContentForRender) + PlatformSuffix)] + public void Text_GetRawContentForRender() + { + var interaction = new AIInteractionText(); + interaction.SetResult(AIAgent.Assistant, "content"); + + Assert.Equal("content", interaction.GetRawContentForRender()); + } + + [Fact(DisplayName = nameof(Text_GetRawReasoningForRender) + PlatformSuffix)] + public void Text_GetRawReasoningForRender() + { + var interaction = new AIInteractionText(); + interaction.SetResult(AIAgent.Assistant, "content", "reasoning"); + + Assert.Equal("reasoning", interaction.GetRawReasoningForRender()); + + var emptyReasoning = new AIInteractionText(); + Assert.Equal(string.Empty, emptyReasoning.GetRawReasoningForRender()); + } + + #endregion + + #region AIInteractionToolCall + + [Fact(DisplayName = nameof(ToolCall_ToStringWithoutIdNameAndArguments) + PlatformSuffix)] + public void ToolCall_ToStringWithoutIdNameAndArguments() + { + var interaction = new AIInteractionToolCall(); + + Assert.Equal("Calling tool", interaction.ToString()); + } + + [Fact(DisplayName = nameof(ToolCall_ToStringWithId) + PlatformSuffix)] + public void ToolCall_ToStringWithId() + { + var interaction = new AIInteractionToolCall + { + Id = "tc_1", + }; + + Assert.Equal("Calling tool (tc_1)", interaction.ToString()); + } + + [Fact(DisplayName = nameof(ToolCall_ToStringWithName) + PlatformSuffix)] + public void ToolCall_ToStringWithName() + { + var interaction = new AIInteractionToolCall + { + Name = "my_tool", + }; + + Assert.Equal("Calling tool my_tool", interaction.ToString()); + } + + [Fact(DisplayName = nameof(ToolCall_ToStringWithArguments) + PlatformSuffix)] + public void ToolCall_ToStringWithArguments() + { + var interaction = new AIInteractionToolCall + { + Id = "tc_1", + Name = "my_tool", + Arguments = new JObject { ["x"] = 1 }, + }; + + var result = interaction.ToString(); + + Assert.Contains("Calling tool (tc_1) my_tool", result); + Assert.Contains("with the following arguments:", result); + Assert.Contains("\"x\": 1", result); + } + + [Fact(DisplayName = nameof(ToolCall_GetStreamKeyWithoutTurnId) + PlatformSuffix)] + public void ToolCall_GetStreamKeyWithoutTurnId() + { + var byId = new AIInteractionToolCall { Id = "tc_1" }; + var byName = new AIInteractionToolCall { Name = "my_tool" }; + var empty = new AIInteractionToolCall(); + + Assert.Equal("tool.call:tc_1", byId.GetStreamKey()); + Assert.Equal("tool.call:my_tool", byName.GetStreamKey()); + Assert.Equal("tool.call:", empty.GetStreamKey()); + } + + [Fact(DisplayName = nameof(ToolCall_GetStreamKeyWithTurnId) + PlatformSuffix)] + public void ToolCall_GetStreamKeyWithTurnId() + { + var interaction = new AIInteractionToolCall + { + TurnId = "turn-1", + Id = "tc_1", + }; + + Assert.Equal("turn:turn-1:tool.call:tc_1", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(ToolCall_GetDedupKey) + PlatformSuffix)] + public void ToolCall_GetDedupKey() + { + var interaction = new AIInteractionToolCall + { + TurnId = "turn-1", + Id = "tc_1", + Arguments = new JObject { ["x"] = 1 }, + }; + + var argsHash = HashUtility.ComputeShortHash(interaction.Arguments.ToString(Newtonsoft.Json.Formatting.None)); + var expected = $"turn:turn-1:tool.call:tc_1:{argsHash}"; + + Assert.Equal(expected, interaction.GetDedupKey()); + } + + [Fact(DisplayName = nameof(ToolCall_GetDedupKeyWithNoArguments) + PlatformSuffix)] + public void ToolCall_GetDedupKeyWithNoArguments() + { + var interaction = new AIInteractionToolCall + { + Id = "tc_1", + }; + + Assert.Equal("tool.call:tc_1:none", interaction.GetDedupKey()); + } + + [Fact(DisplayName = nameof(ToolCall_GetRoleClassForRender) + PlatformSuffix)] + public void ToolCall_GetRoleClassForRender() + { + var interaction = new AIInteractionToolCall(); + + Assert.Equal("tool", interaction.GetRoleClassForRender()); + } + + [Fact(DisplayName = nameof(ToolCall_GetDisplayNameForRender) + PlatformSuffix)] + public void ToolCall_GetDisplayNameForRender() + { + var unnamed = new AIInteractionToolCall(); + var named = new AIInteractionToolCall { Name = "my_tool" }; + + Assert.Equal("Tool Call", unnamed.GetDisplayNameForRender()); + Assert.Equal("Tool Call: my_tool", named.GetDisplayNameForRender()); + } + + [Fact(DisplayName = nameof(ToolCall_GetRawContentForRender) + PlatformSuffix)] + public void ToolCall_GetRawContentForRender() + { + var interaction = new AIInteractionToolCall + { + Arguments = new JObject { ["x"] = 1 }, + }; + + var result = interaction.GetRawContentForRender(); + + Assert.Contains("\"x\": 1", result); + + var empty = new AIInteractionToolCall(); + + Assert.Equal(string.Empty, empty.GetRawContentForRender()); + } + + [Fact(DisplayName = nameof(ToolCall_GetRawReasoningForRender) + PlatformSuffix)] + public void ToolCall_GetRawReasoningForRender() + { + var interaction = new AIInteractionToolCall + { + Reasoning = "some thought", + }; + + Assert.Equal("some thought", interaction.GetRawReasoningForRender()); + + var emptyReasoning = new AIInteractionToolCall(); + Assert.Equal(string.Empty, emptyReasoning.GetRawReasoningForRender()); + } + + #endregion + + #region AIInteractionToolResult + + [Fact(DisplayName = nameof(ToolResult_ToString) + PlatformSuffix)] + public void ToolResult_ToString() + { + var interaction = new AIInteractionToolResult + { + Name = "my_tool", + Result = new JObject { ["ok"] = true }, + }; + + var result = interaction.ToString(); + + Assert.Contains("Tool result from my_tool", result); + Assert.Contains("\"ok\": true", result); + } + + [Fact(DisplayName = nameof(ToolResult_GetStreamKey) + PlatformSuffix)] + public void ToolResult_GetStreamKey() + { + var withoutTurn = new AIInteractionToolResult + { + Id = "tr_1", + }; + + var withTurn = new AIInteractionToolResult + { + TurnId = "turn-1", + Name = "my_tool", + }; + + Assert.Equal("tool.result:tr_1", withoutTurn.GetStreamKey()); + Assert.Equal("turn:turn-1:tool.result:my_tool", withTurn.GetStreamKey()); + } + + [Fact(DisplayName = nameof(ToolResult_GetDedupKey) + PlatformSuffix)] + public void ToolResult_GetDedupKey() + { + var interaction = new AIInteractionToolResult + { + Id = "tr_1", + Result = new JObject { ["ok"] = true }, + }; + + var key = interaction.GetDedupKey(); + + Assert.StartsWith("tool.result:tr_1:", key); + Assert.Equal("tool.result:tr_1:".Length + 16, key.Length); + } + + [Fact(DisplayName = nameof(ToolResult_MessagesPropagation) + PlatformSuffix)] + public void ToolResult_MessagesPropagation() + { + var messages = new List + { + new SHRuntimeMessage( + SHRuntimeMessageSeverity.Warning, + SHRuntimeMessageOrigin.Tool, + SHMessageCode.ToolExecutionError, + "warning message"), + }; + + var interaction = new AIInteractionToolResult + { + Messages = messages, + }; + + Assert.Single(interaction.Messages); + Assert.Equal("warning message", interaction.Messages[0].Message); + } + + #endregion + + #region AIInteractionImage + + [Fact(DisplayName = nameof(Image_CreateVisionInputWithValidUrl) + PlatformSuffix)] + public void Image_CreateVisionInputWithValidUrl() + { + var interaction = new AIInteractionImage(); + interaction.CreateVisionInput("https://example.com/image.png"); + + Assert.Equal("https://example.com/image.png", interaction.ImageUrl.ToString()); + Assert.Contains("[vision input]", interaction.ToString()); + } + + [Fact(DisplayName = nameof(Image_CreateVisionInputWithInvalidUrlThrows) + PlatformSuffix)] + public void Image_CreateVisionInputWithInvalidUrlThrows() + { + var interaction = new AIInteractionImage(); + + Assert.Throws(() => interaction.CreateVisionInput("not a valid url")); + } + + [Fact(DisplayName = nameof(Image_CreateVisionInputWithNullOrEmptyThrows) + PlatformSuffix)] + public void Image_CreateVisionInputWithNullOrEmptyThrows() + { + var interaction = new AIInteractionImage(); + + Assert.Throws(() => interaction.CreateVisionInput(string.Empty)); + Assert.Throws(() => interaction.CreateVisionInput(" ")); + } + + [Fact(DisplayName = nameof(Image_CreateVisionInputFromBase64) + PlatformSuffix)] + public void Image_CreateVisionInputFromBase64() + { + var interaction = new AIInteractionImage(); + interaction.CreateVisionInputFromBase64("base64data"); + + Assert.Equal("base64data", interaction.ImageData); + Assert.Equal("image/png", interaction.MimeType); + + var withMime = new AIInteractionImage(); + withMime.CreateVisionInputFromBase64("base64data", "image/jpeg"); + + Assert.Equal("image/jpeg", withMime.MimeType); + } + + [Fact(DisplayName = nameof(Image_CreateRequestSetsProperties) + PlatformSuffix)] + public void Image_CreateRequestSetsProperties() + { + var interaction = new AIInteractionImage(); + interaction.CreateRequest( + prompt: "a red apple", + size: "512x512", + quality: "hd", + style: "natural", + aspectRatio: "16:9"); + + Assert.Equal("a red apple", interaction.OriginalPrompt); + Assert.Equal("512x512", interaction.ImageSize); + Assert.Equal("hd", interaction.ImageQuality); + Assert.Equal("natural", interaction.ImageStyle); + Assert.Equal("16:9", interaction.AspectRatio); + } + + [Fact(DisplayName = nameof(Image_SetResultWithValidUrl) + PlatformSuffix)] + public void Image_SetResultWithValidUrl() + { + var interaction = new AIInteractionImage(); + interaction.SetResult("https://example.com/image.png"); + + Assert.Equal("https://example.com/image.png", interaction.ImageUrl.ToString()); + } + + [Fact(DisplayName = nameof(Image_SetResultInvalidUrlWithoutImageDataThrows) + PlatformSuffix)] + public void Image_SetResultInvalidUrlWithoutImageDataThrows() + { + var interaction = new AIInteractionImage(); + + Assert.Throws(() => interaction.SetResult("not a valid url")); + } + + [Fact(DisplayName = nameof(Image_SetResultInvalidUrlWithImageDataSucceeds) + PlatformSuffix)] + public void Image_SetResultInvalidUrlWithImageDataSucceeds() + { + var interaction = new AIInteractionImage(); + interaction.CreateRequest("a cat"); + interaction.SetResult("not a valid url", "base64data"); + + Assert.Null(interaction.ImageUrl); + Assert.Equal("base64data", interaction.ImageData); + Assert.Contains("generated from 'a cat'", interaction.ToString()); + Assert.Contains("data:image/png;base64,base64data", interaction.GetRawContentForRender()); + } + + [Fact(DisplayName = nameof(Image_ToStringForVisionInput) + PlatformSuffix)] + public void Image_ToStringForVisionInput() + { + var interaction = new AIInteractionImage(); + interaction.CreateVisionInput("https://example.com/image.png"); + + Assert.Equal("AIInteractionImage (1024x1024) [vision input]", interaction.ToString()); + } + + [Fact(DisplayName = nameof(Image_ToStringForGeneratedImage) + PlatformSuffix)] + public void Image_ToStringForGeneratedImage() + { + var shortPrompt = new AIInteractionImage(); + shortPrompt.CreateRequest("a cat"); + + Assert.Equal("AIInteractionImage (1024x1024) generated from 'a cat'", shortPrompt.ToString()); + + var longPrompt = new AIInteractionImage(); + longPrompt.CreateRequest("a very long prompt that should be truncated in the output"); + + Assert.Contains("generated from '", longPrompt.ToString()); + Assert.Contains("...", longPrompt.ToString()); + } + + [Fact(DisplayName = nameof(Image_GetRoleClassForRender) + PlatformSuffix)] + public void Image_GetRoleClassForRender() + { + var defaultImage = new AIInteractionImage(); + var userImage = new AIInteractionImage { Agent = AIAgent.User }; + + Assert.Equal("assistant", defaultImage.GetRoleClassForRender()); + Assert.Equal("user", userImage.GetRoleClassForRender()); + } + + [Fact(DisplayName = nameof(Image_GetDisplayNameForRender) + PlatformSuffix)] + public void Image_GetDisplayNameForRender() + { + var defaultImage = new AIInteractionImage(); + var userImage = new AIInteractionImage { Agent = AIAgent.User }; + + Assert.Equal("Assistant", defaultImage.GetDisplayNameForRender()); + Assert.Equal("User", userImage.GetDisplayNameForRender()); + } + + [Fact(DisplayName = nameof(Image_GetRawContentForRender) + PlatformSuffix)] + public void Image_GetRawContentForRender() + { + var withUrl = new AIInteractionImage(); + withUrl.SetResult("https://example.com/image.png"); + + Assert.Equal("![generated image](https://example.com/image.png)", withUrl.GetRawContentForRender()); + + var withData = new AIInteractionImage(); + withData.CreateVisionInputFromBase64("data"); + + Assert.Equal("![generated image](data:image/png;base64,data)", withData.GetRawContentForRender()); + + var neither = new AIInteractionImage(); + neither.CreateRequest("a cat"); + + Assert.Contains("generated from 'a cat'", neither.GetRawContentForRender()); + } + + #endregion + + #region AIInteractionRuntimeMessage + + [Fact(DisplayName = nameof(RuntimeMessage_CreateDebugIsNonSurfaceable) + PlatformSuffix)] + public void RuntimeMessage_CreateDebugIsNonSurfaceable() + { + var interaction = AIInteractionRuntimeMessage.CreateDebug("debug content"); + + Assert.Equal(SHRuntimeMessageSeverity.Debug, interaction.Severity); + Assert.False(interaction.Surfaceable); + Assert.Equal("debug content", interaction.Content); + Assert.Equal(AIAgent.Debug, interaction.Agent); + } + + [Fact(DisplayName = nameof(RuntimeMessage_FromRuntimeMessageNullReturnsNull) + PlatformSuffix)] + public void RuntimeMessage_FromRuntimeMessageNullReturnsNull() + { + Assert.Null(AIInteractionRuntimeMessage.FromRuntimeMessage(null!)); + } + + [Fact(DisplayName = nameof(RuntimeMessage_FromRuntimeMessageRoundTrip) + PlatformSuffix)] + public void RuntimeMessage_FromRuntimeMessageRoundTrip() + { + var message = new SHRuntimeMessage( + SHRuntimeMessageSeverity.Warning, + SHRuntimeMessageOrigin.Provider, + SHMessageCode.NetworkTimeout, + "timeout", + false); + + var interaction = AIInteractionRuntimeMessage.FromRuntimeMessage(message); + + Assert.Equal(SHRuntimeMessageSeverity.Warning, interaction.Severity); + Assert.Equal(SHRuntimeMessageOrigin.Provider, interaction.Origin); + Assert.Equal(SHMessageCode.NetworkTimeout, interaction.Code); + Assert.Equal("timeout", interaction.Content); + Assert.False(interaction.Surfaceable); + Assert.Equal(AIAgent.Warning, interaction.Agent); + } + + [Fact(DisplayName = nameof(RuntimeMessage_AgentDerivedFromSeverity) + PlatformSuffix)] + public void RuntimeMessage_AgentDerivedFromSeverity() + { + var error = new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Error }; + var warning = new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Warning }; + var info = new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Info }; + var debug = new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Debug }; + + Assert.Equal(AIAgent.Error, error.Agent); + Assert.Equal(AIAgent.Warning, warning.Agent); + Assert.Equal(AIAgent.Info, info.Agent); + Assert.Equal(AIAgent.Debug, debug.Agent); + } + + [Fact(DisplayName = nameof(RuntimeMessage_ToRuntimeMessage) + PlatformSuffix)] + public void RuntimeMessage_ToRuntimeMessage() + { + var interaction = new AIInteractionRuntimeMessage + { + Severity = SHRuntimeMessageSeverity.Error, + Origin = SHRuntimeMessageOrigin.Tool, + Code = SHMessageCode.ToolExecutionError, + Content = "error content", + Surfaceable = false, + }; + + var message = interaction.ToRuntimeMessage(); + + Assert.Equal(SHRuntimeMessageSeverity.Error, message.Severity); + Assert.Equal(SHRuntimeMessageOrigin.Tool, message.Origin); + Assert.Equal(SHMessageCode.ToolExecutionError, message.Code); + Assert.Equal("error content", message.Message); + Assert.False(message.Surfaceable); + } + + [Fact(DisplayName = nameof(RuntimeMessage_GetRoleClassForRender) + PlatformSuffix)] + public void RuntimeMessage_GetRoleClassForRender() + { + Assert.Equal("error", new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Error }.GetRoleClassForRender()); + Assert.Equal("warning", new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Warning }.GetRoleClassForRender()); + Assert.Equal("info", new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Info }.GetRoleClassForRender()); + Assert.Equal("debug", new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Debug }.GetRoleClassForRender()); + } + + [Fact(DisplayName = nameof(RuntimeMessage_GetDisplayNameForRender) + PlatformSuffix)] + public void RuntimeMessage_GetDisplayNameForRender() + { + Assert.Equal("Error", new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Error }.GetDisplayNameForRender()); + Assert.Equal("Warning", new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Warning }.GetDisplayNameForRender()); + Assert.Equal("Info", new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Info }.GetDisplayNameForRender()); + Assert.Equal("Debug", new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Debug }.GetDisplayNameForRender()); + } + + [Fact(DisplayName = nameof(RuntimeMessage_GetRawContentForRender) + PlatformSuffix)] + public void RuntimeMessage_GetRawContentForRender() + { + var interaction = new AIInteractionRuntimeMessage { Content = "content" }; + + Assert.Equal("content", interaction.GetRawContentForRender()); + + var emptyContent = new AIInteractionRuntimeMessage(); + Assert.Equal(string.Empty, emptyContent.GetRawContentForRender()); + } + + [Fact(DisplayName = nameof(RuntimeMessage_GetRawReasoningForRender) + PlatformSuffix)] + public void RuntimeMessage_GetRawReasoningForRender() + { + var interaction = new AIInteractionRuntimeMessage(); + + Assert.Equal(string.Empty, interaction.GetRawReasoningForRender()); + } + + [Fact(DisplayName = nameof(RuntimeMessage_GetStreamKeyWithoutTurnId) + PlatformSuffix)] + public void RuntimeMessage_GetStreamKeyWithoutTurnId() + { + var interaction = new AIInteractionRuntimeMessage + { + Severity = SHRuntimeMessageSeverity.Info, + Content = "info message", + }; + + var hash = HashUtility.ComputeShortHash("info message"); + + Assert.Equal($"info:{hash}", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(RuntimeMessage_GetStreamKeyWithTurnId) + PlatformSuffix)] + public void RuntimeMessage_GetStreamKeyWithTurnId() + { + var interaction = new AIInteractionRuntimeMessage + { + TurnId = "turn-1", + Severity = SHRuntimeMessageSeverity.Warning, + Content = "warning message", + }; + + var hash = HashUtility.ComputeShortHash("warning message"); + + Assert.Equal($"turn:turn-1:warning:{hash}", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(RuntimeMessage_GetDedupKey) + PlatformSuffix)] + public void RuntimeMessage_GetDedupKey() + { + var interaction = new AIInteractionRuntimeMessage + { + Severity = SHRuntimeMessageSeverity.Info, + Content = "info message", + }; + + Assert.Equal(interaction.GetStreamKey(), interaction.GetDedupKey()); + } + + #endregion + + #region AIInteractionAudio + + [Fact(DisplayName = nameof(Audio_GetAudioSizeWithData) + PlatformSuffix)] + public void Audio_GetAudioSizeWithData() + { + var interaction = new AIInteractionAudio + { + Data = new byte[] { 1, 2, 3 }, + }; + + Assert.Equal(3, interaction.GetAudioSize()); + } + + [Fact(DisplayName = nameof(Audio_GetAudioSizeWithMissingFile) + PlatformSuffix)] + public void Audio_GetAudioSizeWithMissingFile() + { + var interaction = new AIInteractionAudio + { + FilePath = Path.Combine(Path.GetTempPath(), $"missing-{Guid.NewGuid()}.wav"), + }; + + Assert.Equal(0, interaction.GetAudioSize()); + } + + [Fact(DisplayName = nameof(Audio_GetAudioSizeWithNoData) + PlatformSuffix)] + public void Audio_GetAudioSizeWithNoData() + { + var interaction = new AIInteractionAudio(); + + Assert.Equal(0, interaction.GetAudioSize()); + } + + [Fact(DisplayName = nameof(Audio_GetStreamKeyForData) + PlatformSuffix)] + public void Audio_GetStreamKeyForData() + { + var data = new byte[] { 1, 2, 3 }; + var interaction = new AIInteractionAudio + { + Data = data, + }; + + var hash = HashUtility.ComputeShortHash(Convert.ToBase64String(data)); + + Assert.Equal($"audio:{hash}", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(Audio_GetStreamKeyForFile) + PlatformSuffix)] + public void Audio_GetStreamKeyForFile() + { + var interaction = new AIInteractionAudio + { + FilePath = @"C:\audio.wav", + }; + + Assert.Equal("audio:C:\\audio.wav", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(Audio_GetStreamKeyForEmpty) + PlatformSuffix)] + public void Audio_GetStreamKeyForEmpty() + { + var interaction = new AIInteractionAudio(); + + Assert.Equal("audio:empty", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(Audio_GetStreamKeyWithTurnId) + PlatformSuffix)] + public void Audio_GetStreamKeyWithTurnId() + { + var interaction = new AIInteractionAudio + { + TurnId = "turn-1", + FilePath = @"C:\audio.wav", + }; + + Assert.Equal("turn:turn-1:audio:C:\\audio.wav", interaction.GetStreamKey()); + } + + [Fact(DisplayName = nameof(Audio_GetDedupKey) + PlatformSuffix)] + public void Audio_GetDedupKey() + { + var interaction = new AIInteractionAudio + { + FilePath = @"C:\audio.wav", + MimeType = "audio/wav", + }; + + Assert.Equal("audio:C:\\audio.wav:audio/wav", interaction.GetDedupKey()); + + var unknownMime = new AIInteractionAudio + { + FilePath = @"C:\audio.wav", + }; + + Assert.Equal("audio:C:\\audio.wav:unknown", unknownMime.GetDedupKey()); + } + + [Fact(DisplayName = nameof(Audio_ToString) + PlatformSuffix)] + public void Audio_ToString() + { + var inMemory = new AIInteractionAudio + { + Data = new byte[] { 1, 2, 3 }, + MimeType = "audio/wav", + LanguageHint = "en", + }; + + Assert.Equal("Audio(audio/wav, in-memory (3 bytes)) [en]", inMemory.ToString()); + + var fromFile = new AIInteractionAudio + { + FilePath = @"C:\audio.wav", + MimeType = "audio/wav", + }; + + Assert.Equal("Audio(audio/wav, C:\\audio.wav)", fromFile.ToString()); + + var empty = new AIInteractionAudio { MimeType = "audio/wav" }; + + Assert.Equal("Audio(audio/wav, unknown)", empty.ToString()); + } + + #endregion + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Requests/AIRequestBaseTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Requests/AIRequestBaseTests.cs new file mode 100644 index 000000000..edb60eb59 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Requests/AIRequestBaseTests.cs @@ -0,0 +1,231 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Core.Requests +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.Diagnostics; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AIRequestBaseTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "Constructor_DefaultValues [Windows]")] +#else + [Fact(DisplayName = "Constructor_DefaultValues [Core]")] +#endif + public void Constructor_DefaultValues() + { + var request = new AIRequestBase(); + + Assert.Equal(AICapability.None, request.Capability); + Assert.False(request.WantsStreaming); + Assert.Equal(AIRequestKind.Generation, request.RequestKind); + Assert.Equal(AIBody.Empty, request.Body); + Assert.Equal(string.Empty, request.Model); + Assert.Null(request.Provider); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ForceToolCall_ForceToolNameTriggersIt [Windows]")] +#else + [Fact(DisplayName = "ForceToolCall_ForceToolNameTriggersIt [Core]")] +#endif + public void ForceToolCall_ForceToolNameTriggersIt() + { + var request = new AIRequestBase(); + + Assert.False(request.ForceToolCall); + Assert.True(string.IsNullOrEmpty(request.ForceToolName)); + + request.ForceToolName = "gh_get"; + Assert.True(request.ForceToolCall); + + request.ForceToolName = string.Empty; + Assert.False(request.ForceToolCall); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ForceToolCall_ExplicitSetterWorks [Windows]")] +#else + [Fact(DisplayName = "ForceToolCall_ExplicitSetterWorks [Core]")] +#endif + public void ForceToolCall_ExplicitSetterWorks() + { + var request = new AIRequestBase(); + + request.ForceToolCall = true; + Assert.True(request.ForceToolCall); + + request.ForceToolCall = false; + Assert.False(request.ForceToolCall); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ForceToolCall_NameTakesPrecedenceOverExplicitFalse [Windows]")] +#else + [Fact(DisplayName = "ForceToolCall_NameTakesPrecedenceOverExplicitFalse [Core]")] +#endif + public void ForceToolCall_NameTakesPrecedenceOverExplicitFalse() + { + var request = new AIRequestBase(); + + request.ForceToolName = "gh_get"; + request.ForceToolCall = false; + + Assert.True(request.ForceToolCall); + + request.ForceToolName = string.Empty; + Assert.False(request.ForceToolCall); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Initialize_SetsProviderModelEndpointBodyCapabilityAndParameters [Windows]")] +#else + [Fact(DisplayName = "Initialize_SetsProviderModelEndpointBodyCapabilityAndParameters [Core]")] +#endif + public void Initialize_SetsProviderModelEndpointBodyCapabilityAndParameters() + { + var body = AIBodyBuilder.Create() + .AddUser("hello") + .Build(); + + var request = new AIRequestBase(); + request.Initialize( + provider: null!, + model: "test-model", + body: body, + endpoint: "https://test/endpoint", + capability: AICapability.None); + + Assert.Null(request.Provider); + Assert.Equal("test-model", request.Model); + Assert.Equal("https://test/endpoint", request.Endpoint); + Assert.Same(body, request.Body); + Assert.Equal(AICapability.None, request.Capability); + Assert.NotNull(request.Parameters); + Assert.Equal("test-model", request.Parameters.Model); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "IsValid_ValidBodyWithTurnIds [Windows]")] +#else + [Fact(DisplayName = "IsValid_ValidBodyWithTurnIds [Core]")] +#endif + public void IsValid_ValidBodyWithTurnIds() + { + var request = new AIRequestBase(); + request.Body = AIBodyBuilder.Create() + .AddUser("hello") + .Build(); + + var (isValid, errors) = request.IsValid(); + + Assert.True(isValid); + Assert.Empty(errors); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "IsValid_InvalidBodyMissingTurnId [Windows]")] +#else + [Fact(DisplayName = "IsValid_InvalidBodyMissingTurnId [Core]")] +#endif + public void IsValid_InvalidBodyMissingTurnId() + { + var request = new AIRequestBase(); + request.Body = new AIBody( + new List + { + new AIInteractionText { Agent = AIAgent.User, Content = "no turn id" }, + }, + "-*", + "-*", + null!, + new List()); + + var (isValid, errors) = request.IsValid(); + + Assert.False(isValid); + Assert.Contains(errors, e => e.Message.Contains("TurnId", StringComparison.Ordinal)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Messages_SortsBySeverity [Windows]")] +#else + [Fact(DisplayName = "Messages_SortsBySeverity [Core]")] +#endif + public void Messages_SortsBySeverity() + { + var request = new AIRequestBase(); + request.Body = AIBodyBuilder.Create() + .AddUser("hello") + .Build(); + request.Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Info, SHRuntimeMessageOrigin.Request, SHMessageCode.Unknown, "info"), + new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Request, SHMessageCode.Unknown, "error"), + new SHRuntimeMessage(SHRuntimeMessageSeverity.Warning, SHRuntimeMessageOrigin.Request, SHMessageCode.Unknown, "warning"), + }; + + var messages = request.Messages; + + Assert.Equal(SHRuntimeMessageSeverity.Error, messages[0].Severity); + Assert.Equal(SHRuntimeMessageSeverity.Warning, messages[1].Severity); + Assert.Equal(SHRuntimeMessageSeverity.Info, messages[2].Severity); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Exec_ThrowsNotImplementedException [Windows]")] +#else + [Fact(DisplayName = "Exec_ThrowsNotImplementedException [Core]")] +#endif + public async Task Exec_ThrowsNotImplementedException() + { + var request = new AIRequestBase(); + + var ex = await Assert.ThrowsAsync(() => request.Exec()).ConfigureAwait(false); + Assert.Contains("Exec()", ex.Message, StringComparison.Ordinal); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "SkipMetricsValidation_CanBeSet [Windows]")] +#else + [Fact(DisplayName = "SkipMetricsValidation_CanBeSet [Core]")] +#endif + public void SkipMetricsValidation_CanBeSet() + { + var request = new AIRequestBase(); + + Assert.False(request.SkipMetricsValidation); + + request.SkipMetricsValidation = true; + + Assert.True(request.SkipMetricsValidation); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Requests/AIRequestCallTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Requests/AIRequestCallTests.cs new file mode 100644 index 000000000..79795c70a --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Requests/AIRequestCallTests.cs @@ -0,0 +1,159 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Core.Requests +{ + using System.Collections.Generic; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AIModels; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AIRequestCallTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "Constructor_Defaults [Windows]")] +#else + [Fact(DisplayName = "Constructor_Defaults [Core]")] +#endif + public void Constructor_Defaults() + { + var request = new AIRequestCall(); + + Assert.Equal("POST", request.HttpMethod); + Assert.Equal("bearer", request.Authentication); + Assert.Equal("application/json", request.ContentType); + Assert.Empty(request.Headers); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Headers_AreCaseInsensitive [Windows]")] +#else + [Fact(DisplayName = "Headers_AreCaseInsensitive [Core]")] +#endif + public void Headers_AreCaseInsensitive() + { + var request = new AIRequestCall(); + + request.Headers["x-custom"] = "value"; + + Assert.Single(request.Headers); + Assert.Equal("value", request.Headers["X-CUSTOM"]); + Assert.Equal("value", request.Headers["X-Custom"]); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Capability_InfersJsonOutput [Windows]")] +#else + [Fact(DisplayName = "Capability_InfersJsonOutput [Core]")] +#endif + public void Capability_InfersJsonOutput() + { + var request = new AIRequestCall(); + request.Body = AIBodyBuilder.Create() + .WithJsonOutputSchema("{ \"type\": \"object\" }") + .Build(); + + Assert.True(request.Capability.HasFlag(AICapability.Text2Text)); + Assert.True(request.Capability.HasFlag(AICapability.JsonOutput)); + Assert.False(request.Capability.HasFlag(AICapability.FunctionCalling)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Capability_InfersFunctionCalling [Windows]")] +#else + [Fact(DisplayName = "Capability_InfersFunctionCalling [Core]")] +#endif + public void Capability_InfersFunctionCalling() + { + var request = new AIRequestCall(); + request.Body = AIBodyBuilder.Create() + .WithToolFilter("+gh_*") + .Build(); + + Assert.True(request.Capability.HasFlag(AICapability.Text2Text)); + Assert.True(request.Capability.HasFlag(AICapability.FunctionCalling)); + Assert.False(request.Capability.HasFlag(AICapability.JsonOutput)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Capability_InfersBoth [Windows]")] +#else + [Fact(DisplayName = "Capability_InfersBoth [Core]")] +#endif + public void Capability_InfersBoth() + { + var request = new AIRequestCall(); + request.Body = AIBodyBuilder.Create() + .WithJsonOutputSchema("{ \"type\": \"object\" }") + .WithToolFilter("+gh_*") + .Build(); + + Assert.True(request.Capability.HasFlag(AICapability.Text2Text)); + Assert.True(request.Capability.HasFlag(AICapability.JsonOutput)); + Assert.True(request.Capability.HasFlag(AICapability.FunctionCalling)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Initialize_SetsBodyEndpointAndParameters [Windows]")] +#else + [Fact(DisplayName = "Initialize_SetsBodyEndpointAndParameters [Core]")] +#endif + public void Initialize_SetsBodyEndpointAndParameters() + { + var body = AIBodyBuilder.Create() + .AddUser("hello") + .Build(); + + var request = new AIRequestCall(); + request.Initialize( + provider: null!, + model: "test-model", + body: body, + endpoint: "https://test/endpoint", + capability: AICapability.None); + + Assert.Same(body, request.Body); + Assert.Equal("https://test/endpoint", request.Endpoint); + Assert.NotNull(request.Parameters); + Assert.Equal("test-model", request.Parameters.Model); + Assert.Equal("test-model", request.Model); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "RequestKind_DefaultsToGenerationAndCanBeSet [Windows]")] +#else + [Fact(DisplayName = "RequestKind_DefaultsToGenerationAndCanBeSet [Core]")] +#endif + public void RequestKind_DefaultsToGenerationAndCanBeSet() + { + var request = new AIRequestCall(); + + Assert.Equal(AIRequestKind.Generation, request.RequestKind); + + request.RequestKind = AIRequestKind.Backoffice; + + Assert.Equal(AIRequestKind.Backoffice, request.RequestKind); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Returns/AIReturnTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Returns/AIReturnTests.cs new file mode 100644 index 000000000..344bafceb --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Core/Returns/AIReturnTests.cs @@ -0,0 +1,413 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Core.Returns +{ + using System; + using System.Collections.Generic; + using System.Linq; + using SmartHopper.ProviderSdk.AICall.Core.Base; + using SmartHopper.ProviderSdk.AICall.Core.Interactions; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.Diagnostics; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AIReturnTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "Constructor_DefaultValues [Windows]")] +#else + [Fact(DisplayName = "Constructor_DefaultValues [Core]")] +#endif + public void Constructor_DefaultValues() + { + var ret = new AIReturn(); + + Assert.Equal(AICallStatus.Idle, ret.Status); + Assert.Equal(AIBody.Empty, ret.Body); + Assert.Null(ret.Request); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "SetBody_AIBody_AssignsBody [Windows]")] +#else + [Fact(DisplayName = "SetBody_AIBody_AssignsBody [Core]")] +#endif + public void SetBody_AIBody_AssignsBody() + { + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "hello") + .Build(); + + var ret = new AIReturn(); + ret.SetBody(body); + + Assert.Equal(body, ret.Body); + Assert.NotEqual(AIBody.Empty, ret.Body); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "SetBody_Interactions_BuildsBody [Windows]")] +#else + [Fact(DisplayName = "SetBody_Interactions_BuildsBody [Core]")] +#endif + public void SetBody_Interactions_BuildsBody() + { + var interactions = new List + { + new AIInteractionText { Agent = AIAgent.Assistant, Content = "hello" }, + }; + + var ret = new AIReturn(); + ret.SetBody(interactions); + + Assert.NotEqual(AIBody.Empty, ret.Body); + Assert.Equal(1, ret.Body.InteractionsCount); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddRuntimeMessage_AddsToMessages [Windows]")] +#else + [Fact(DisplayName = "AddRuntimeMessage_AddsToMessages [Core]")] +#endif + public void AddRuntimeMessage_AddsToMessages() + { + var ret = new AIReturn + { + SkipRequestValidation = true, + SkipMetricsValidation = true, + }; + + ret.AddRuntimeMessage(SHRuntimeMessageSeverity.Warning, SHRuntimeMessageOrigin.Return, "a warning"); + + Assert.Single(ret.Messages); + Assert.Contains("a warning", ret.Messages[0].Message, StringComparison.Ordinal); + Assert.Equal(SHRuntimeMessageSeverity.Warning, ret.Messages[0].Severity); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddRuntimeMessage_Error_MakesSuccessFalse [Windows]")] +#else + [Fact(DisplayName = "AddRuntimeMessage_Error_MakesSuccessFalse [Core]")] +#endif + public void AddRuntimeMessage_Error_MakesSuccessFalse() + { + var ret = new AIReturn + { + SkipRequestValidation = true, + SkipMetricsValidation = true, + }; + + ret.AddRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Return, "an error"); + + Assert.False(ret.Success); + Assert.Contains(ret.Messages, m => m.Severity == SHRuntimeMessageSeverity.Error); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "AddRuntimeMessage_Warning_KeepsSuccessTrue [Windows]")] +#else + [Fact(DisplayName = "AddRuntimeMessage_Warning_KeepsSuccessTrue [Core]")] +#endif + public void AddRuntimeMessage_Warning_KeepsSuccessTrue() + { + var ret = new AIReturn + { + SkipRequestValidation = true, + SkipMetricsValidation = true, + }; + + ret.AddRuntimeMessage(SHRuntimeMessageSeverity.Warning, SHRuntimeMessageOrigin.Return, "a warning"); + + Assert.True(ret.Success); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Messages_IncludesBodyMessages [Windows]")] +#else + [Fact(DisplayName = "Messages_IncludesBodyMessages [Core]")] +#endif + public void Messages_IncludesBodyMessages() + { + var image = new AIInteractionImage + { + Agent = AIAgent.User, + OriginalPrompt = "test image", + Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Warning, SHRuntimeMessageOrigin.Tool, SHMessageCode.Unknown, "body warning"), + }, + }; + + var body = AIBodyBuilder.Create() + .Add(image) + .Build(); + + var ret = new AIReturn + { + Request = CreateFakeRequest(), + SkipRequestValidation = true, + SkipMetricsValidation = true, + }; + ret.SetBody(body); + + Assert.Contains(ret.Messages, m => m.Message == "body warning" && m.Severity == SHRuntimeMessageSeverity.Warning); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Messages_IncludesRequestMessages [Windows]")] +#else + [Fact(DisplayName = "Messages_IncludesRequestMessages [Core]")] +#endif + public void Messages_IncludesRequestMessages() + { + var request = CreateFakeRequest(); + request.Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Info, SHRuntimeMessageOrigin.Request, SHMessageCode.Unknown, "request info"), + }; + + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "hello", new AIMetrics { Provider = "openai", Model = "gpt-test", FinishReason = "stop" }) + .Build(); + + var ret = new AIReturn + { + Request = request, + SkipRequestValidation = true, + SkipMetricsValidation = true, + }; + ret.SetBody(body); + + Assert.Contains(ret.Messages, m => m.Message == "request info" && m.Severity == SHRuntimeMessageSeverity.Info); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Messages_IncludesValidationErrors [Windows]")] +#else + [Fact(DisplayName = "Messages_IncludesValidationErrors [Core]")] +#endif + public void Messages_IncludesValidationErrors() + { + var interaction = new AIInteractionText { Agent = AIAgent.User, Content = "missing turn id" }; + var invalidBody = new AIBody( + new List { interaction }, + "-*", + "-*", + null!, + new List()); + + var request = CreateFakeRequest(); + request.Body = invalidBody; + + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "hello", new AIMetrics { Provider = "openai", Model = "gpt-test", FinishReason = "stop" }) + .Build(); + + var ret = new AIReturn + { + Request = request, + }; + ret.SetBody(body); + + Assert.Contains(ret.Messages, m => m.Message.Contains("TurnId", StringComparison.Ordinal)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Messages_SortsBySeverity [Windows]")] +#else + [Fact(DisplayName = "Messages_SortsBySeverity [Core]")] +#endif + public void Messages_SortsBySeverity() + { + var image = new AIInteractionImage + { + Agent = AIAgent.User, + OriginalPrompt = "test image", + Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Warning, SHRuntimeMessageOrigin.Tool, SHMessageCode.Unknown, "body warning"), + }, + }; + + var body = AIBodyBuilder.Create() + .Add(image) + .Build(); + + var request = CreateFakeRequest(); + request.Messages = new List + { + new SHRuntimeMessage(SHRuntimeMessageSeverity.Info, SHRuntimeMessageOrigin.Request, SHMessageCode.Unknown, "request info"), + }; + + var ret = new AIReturn + { + Request = request, + SkipRequestValidation = true, + SkipMetricsValidation = true, + }; + ret.SetBody(body); + ret.AddRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Return, "private error"); + + var messages = ret.Messages; + + Assert.Equal(SHRuntimeMessageSeverity.Error, messages[0].Severity); + Assert.Equal(SHRuntimeMessageSeverity.Warning, messages[1].Severity); + Assert.Equal(SHRuntimeMessageSeverity.Info, messages[2].Severity); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "IsValid_ValidWithNonEmptyBodyAndSkipFlags [Windows]")] +#else + [Fact(DisplayName = "IsValid_ValidWithNonEmptyBodyAndSkipFlags [Core]")] +#endif + public void IsValid_ValidWithNonEmptyBodyAndSkipFlags() + { + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "hello") + .Build(); + + var ret = new AIReturn + { + SkipRequestValidation = true, + SkipMetricsValidation = true, + }; + ret.SetBody(body); + + var (isValid, _) = ret.IsValid(); + + Assert.True(isValid); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "IsValid_ValidWithNonEmptyBodyAndValidMetrics [Windows]")] +#else + [Fact(DisplayName = "IsValid_ValidWithNonEmptyBodyAndValidMetrics [Core]")] +#endif + public void IsValid_ValidWithNonEmptyBodyAndValidMetrics() + { + var body = AIBodyBuilder.Create() + .AddText( + AIAgent.Assistant, + "hello", + new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + FinishReason = "stop", + InputTokensPrompt = 1, + OutputTokensGeneration = 1, + }) + .Build(); + + var ret = new AIReturn + { + Request = CreateFakeRequest(), + }; + ret.SetBody(body); + + var (isValid, _) = ret.IsValid(); + + Assert.True(isValid); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "IsValid_InvalidWhenBodyEmptyAndNoPrivateMessages [Windows]")] +#else + [Fact(DisplayName = "IsValid_InvalidWhenBodyEmptyAndNoPrivateMessages [Core]")] +#endif + public void IsValid_InvalidWhenBodyEmptyAndNoPrivateMessages() + { + var ret = new AIReturn + { + Request = CreateFakeRequest(), + SkipRequestValidation = true, + SkipMetricsValidation = true, + }; + + var (isValid, errors) = ret.IsValid(); + + Assert.False(isValid); + Assert.Contains(errors, e => e.Message.Contains("Either body or messages must be set", StringComparison.Ordinal)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "CreateSuccess_SetsStatusBodyAndMetrics [Windows]")] +#else + [Fact(DisplayName = "CreateSuccess_SetsStatusBodyAndMetrics [Core]")] +#endif + public void CreateSuccess_SetsStatusBodyAndMetrics() + { + var body = AIBodyBuilder.Create() + .AddText(AIAgent.Assistant, "hello") + .Build(); + + var request = CreateFakeRequest(); + request.Provider = "test-provider"; + + var ret = new AIReturn(); + ret.CreateSuccess(body, request); + + Assert.Equal(AICallStatus.Finished, ret.Status); + Assert.NotEqual(AIBody.Empty, ret.Body); + Assert.Equal(1, ret.Body.InteractionsCount); + Assert.Equal("test-provider", ret.Body.Interactions[0].Metrics.Provider); + Assert.Equal("test-model", ret.Body.Interactions[0].Metrics.Model); + Assert.Equal("test-provider", ret.Metrics.Provider); + Assert.Equal("test-model", ret.Metrics.Model); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "CreateError_AddsErrorAndSetsStatusFinished [Windows]")] +#else + [Fact(DisplayName = "CreateError_AddsErrorAndSetsStatusFinished [Core]")] +#endif + public void CreateError_AddsErrorAndSetsStatusFinished() + { + var ret = new AIReturn(); + ret.CreateError("something went wrong", CreateFakeRequest()); + + ret.Request = CreateFakeRequest(); + ret.SkipRequestValidation = true; + ret.SkipMetricsValidation = true; + + Assert.Equal(AICallStatus.Finished, ret.Status); + Assert.False(ret.Success); + Assert.Contains(ret.Messages, m => m.Severity == SHRuntimeMessageSeverity.Error && m.Message.Contains("something went wrong", StringComparison.Ordinal)); + } + + private static AIRequestBase CreateFakeRequest() + { + return new AIRequestBase + { + Capability = AICapability.None, + Provider = null, + WantsStreaming = false, + Body = AIBody.Empty, + Model = "test-model", + }; + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Metrics/AICostCalculatorTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Metrics/AICostCalculatorTests.cs new file mode 100644 index 000000000..680a9dab8 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Metrics/AICostCalculatorTests.cs @@ -0,0 +1,285 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Metrics +{ + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.Tests.TestHelpers; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AICostCalculatorTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_UnknownPricing_ReturnsZero [Windows]")] +#else + [Fact(DisplayName = "Calculate_UnknownPricing_ReturnsZero [Core]")] +#endif + public void Calculate_UnknownPricing_ReturnsZero() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 1000, + OutputTokensGeneration = 500, + }; + + var cost = AICostCalculator.Calculate(metrics); + + Assert.Equal(0m, cost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_NegativeOrFreePrices_ReturnsZero [Windows]")] +#else + [Fact(DisplayName = "Calculate_NegativeOrFreePrices_ReturnsZero [Core]")] +#endif + public void Calculate_NegativeOrFreePrices_ReturnsZero() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("openai", "free-model", new AIModelPricing { Prompt = 0m, Completion = -1m }); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "free-model", + InputTokensPrompt = 1000, + OutputTokensGeneration = 500, + }; + + var cost = AICostCalculator.Calculate(metrics); + + Assert.Equal(0m, cost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_PromptAndCompletion_ReturnsExpectedCost [Windows]")] +#else + [Fact(DisplayName = "Calculate_PromptAndCompletion_ReturnsExpectedCost [Core]")] +#endif + public void Calculate_PromptAndCompletion_ReturnsExpectedCost() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("openai", "gpt-test", new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.000010m, + }); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 1000, + OutputTokensGeneration = 500, + }; + + var cost = AICostCalculator.Calculate(metrics); + + var expected = (1000m * 0.0000025m) + (500m * 0.000010m); + Assert.Equal(expected, cost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_CacheBuckets_ArePriced [Windows]")] +#else + [Fact(DisplayName = "Calculate_CacheBuckets_ArePriced [Core]")] +#endif + public void Calculate_CacheBuckets_ArePriced() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("anthropic", "claude-test", new AIModelPricing + { + Prompt = 0.000003m, + InputCacheRead = 0.0000003m, + InputCacheWrite = 0.00000375m, + Completion = 0.000015m, + }); + + var metrics = new AIMetrics + { + Provider = "anthropic", + Model = "claude-test", + InputTokensPrompt = 100, + InputTokensCached = 50, + InputTokensCacheWrite = 20, + OutputTokensGeneration = 30, + }; + + var cost = AICostCalculator.Calculate(metrics); + + var expected = (100m * 0.000003m) + + (50m * 0.0000003m) + + (20m * 0.00000375m) + + (30m * 0.000015m); + Assert.Equal(expected, cost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_ReasoningPricedWhenInternalReasoningIsSet [Windows]")] +#else + [Fact(DisplayName = "Calculate_ReasoningPricedWhenInternalReasoningIsSet [Core]")] +#endif + public void Calculate_ReasoningPricedWhenInternalReasoningIsSet() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("google", "gemini-test", new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000004m, + InternalReasoning = 0.000008m, + }); + + var metrics = new AIMetrics + { + Provider = "google", + Model = "gemini-test", + InputTokensPrompt = 1000, + OutputTokensGeneration = 200, + OutputTokensReasoning = 50, + }; + + var cost = AICostCalculator.Calculate(metrics); + + var expected = (1000m * 0.000001m) + + (200m * 0.000004m) + + (50m * 0.000008m); + Assert.Equal(expected, cost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_MissingCategoryPrice_ContributesZero [Windows]")] +#else + [Fact(DisplayName = "Calculate_MissingCategoryPrice_ContributesZero [Core]")] +#endif + public void Calculate_MissingCategoryPrice_ContributesZero() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("openai", "gpt-test", new AIModelPricing + { + Prompt = 0.000002m, + Completion = 0.000006m, + }); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 100, + InputTokensCached = 50, + InputTokensCacheWrite = 20, + OutputTokensGeneration = 30, + OutputTokensReasoning = 10, + }; + + var cost = AICostCalculator.Calculate(metrics); + + var expected = (100m * 0.000002m) + (30m * 0.000006m); + Assert.Equal(expected, cost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_EstimatedInputTokensFallback_WhenNoActualInput [Windows]")] +#else + [Fact(DisplayName = "Calculate_EstimatedInputTokensFallback_WhenNoActualInput [Core]")] +#endif + public void Calculate_EstimatedInputTokensFallback_WhenNoActualInput() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("openai", "gpt-test", new AIModelPricing { Prompt = 0.000001m }); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + EstimatedInputTokens = 1234, + }; + + var cost = AICostCalculator.Calculate(metrics); + + Assert.Equal(1234m * 0.000001m, cost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_EstimatedOutputTokensFallback_WhenNoActualOutput [Windows]")] +#else + [Fact(DisplayName = "Calculate_EstimatedOutputTokensFallback_WhenNoActualOutput [Core]")] +#endif + public void Calculate_EstimatedOutputTokensFallback_WhenNoActualOutput() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("openai", "gpt-test", new AIModelPricing { Completion = 0.000005m }); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + EstimatedOutputTokens = 567, + }; + + var cost = AICostCalculator.Calculate(metrics); + + Assert.Equal(567m * 0.000005m, cost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Calculate_FallsBackPerSide_IndependentBuckets [Windows]")] +#else + [Fact(DisplayName = "Calculate_FallsBackPerSide_IndependentBuckets [Core]")] +#endif + public void Calculate_FallsBackPerSide_IndependentBuckets() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("openai", "gpt-test", new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000004m, + }); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 100, + EstimatedOutputTokens = 200, + }; + + var cost = AICostCalculator.Calculate(metrics); + + var expected = (100m * 0.000001m) + (200m * 0.000004m); + Assert.Equal(expected, cost); + } + + private static void RegisterPricing(string provider, string model, AIModelPricing pricing) + { + AIModelCapabilityRegistry.Instance.SetCapabilities(new AIModelCapabilities + { + Provider = provider, + Model = model, + Pricing = pricing, + }); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AICall/Metrics/AIMetricsTests.cs b/src/SmartHopper.ProviderSdk.Tests/AICall/Metrics/AIMetricsTests.cs new file mode 100644 index 000000000..0c67d025c --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AICall/Metrics/AIMetricsTests.cs @@ -0,0 +1,260 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AICall.Metrics +{ + using SmartHopper.ProviderSdk.AICall.Metrics; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.Tests.TestHelpers; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AIMetricsTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "InputTokens_SumsInputBuckets [Windows]")] +#else + [Fact(DisplayName = "InputTokens_SumsInputBuckets [Core]")] +#endif + public void InputTokens_SumsInputBuckets() + { + var metrics = new AIMetrics + { + InputTokensPrompt = 100, + InputTokensCached = 50, + InputTokensCacheWrite = 25, + }; + + Assert.Equal(175, metrics.InputTokens); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "OutputTokens_SumsOutputBuckets [Windows]")] +#else + [Fact(DisplayName = "OutputTokens_SumsOutputBuckets [Core]")] +#endif + public void OutputTokens_SumsOutputBuckets() + { + var metrics = new AIMetrics + { + OutputTokensGeneration = 80, + OutputTokensReasoning = 20, + }; + + Assert.Equal(100, metrics.OutputTokens); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "TotalTokens_SumsInputAndOutput [Windows]")] +#else + [Fact(DisplayName = "TotalTokens_SumsInputAndOutput [Core]")] +#endif + public void TotalTokens_SumsInputAndOutput() + { + var metrics = new AIMetrics + { + InputTokensPrompt = 100, + OutputTokensGeneration = 50, + }; + + Assert.Equal(150, metrics.TotalTokens); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "EffectiveTotalTokens_UsesMaximum [Windows]")] +#else + [Fact(DisplayName = "EffectiveTotalTokens_UsesMaximum [Core]")] +#endif + public void EffectiveTotalTokens_UsesMaximum() + { + var metrics = new AIMetrics + { + InputTokensPrompt = 100, + OutputTokensGeneration = 50, + EstimatedInputTokens = 500, + EstimatedOutputTokens = 100, + }; + + Assert.Equal(600, metrics.EffectiveTotalTokens); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "IsValid_ValidMetrics [Windows]")] +#else + [Fact(DisplayName = "IsValid_ValidMetrics [Core]")] +#endif + public void IsValid_ValidMetrics() + { + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + FinishReason = "stop", + InputTokensPrompt = 10, + OutputTokensGeneration = 5, + }; + + Assert.True(metrics.IsValid().IsValid); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "IsValid_InvalidMetrics [Windows]")] +#else + [Fact(DisplayName = "IsValid_InvalidMetrics [Core]")] +#endif + public void IsValid_InvalidMetrics() + { + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = -1, + }; + + Assert.False(metrics.IsValid().IsValid); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Combine_MergesAndAddsTokenCounts [Windows]")] +#else + [Fact(DisplayName = "Combine_MergesAndAddsTokenCounts [Core]")] +#endif + public void Combine_MergesAndAddsTokenCounts() + { + var first = new AIMetrics + { + Provider = "openai", + Model = "gpt-4", + InputTokensPrompt = 100, + OutputTokensGeneration = 50, + }; + + var second = new AIMetrics + { + Provider = "openai", + Model = "gpt-4", + InputTokensPrompt = 50, + OutputTokensGeneration = 25, + }; + + first.Combine(second); + + Assert.Equal(150, first.InputTokensPrompt); + Assert.Equal(75, first.OutputTokensGeneration); + Assert.Equal(225, first.TotalTokens); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "Combine_SumsEstimatedCost [Windows]")] +#else + [Fact(DisplayName = "Combine_SumsEstimatedCost [Core]")] +#endif + public void Combine_SumsEstimatedCost() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("openai", "gpt-test", 0.000001m, 0.000004m); + + var first = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 100, + OutputTokensGeneration = 50, + }; + + var second = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 50, + OutputTokensGeneration = 25, + }; + + var firstCost = first.EstimatedCost; + var secondCost = second.EstimatedCost; + + first.Combine(second); + + Assert.Equal(firstCost + secondCost, first.EstimatedCost); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "EstimatedCost_CachesAfterFirstAccess [Windows]")] +#else + [Fact(DisplayName = "EstimatedCost_CachesAfterFirstAccess [Core]")] +#endif + public void EstimatedCost_CachesAfterFirstAccess() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + RegisterPricing("openai", "gpt-test", 0.000001m, 0.000004m); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 100, + OutputTokensGeneration = 50, + }; + + var cost1 = metrics.EstimatedCost; + var cost2 = metrics.EstimatedCost; + + Assert.Equal(cost1, cost2); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ContextUsagePercent_ComputesFromContextLimit [Windows]")] +#else + [Fact(DisplayName = "ContextUsagePercent_ComputesFromContextLimit [Core]")] +#endif + public void ContextUsagePercent_ComputesFromContextLimit() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + AIModelCapabilityRegistry.Instance.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-test", + ContextLimit = 10000, + }); + + var metrics = new AIMetrics + { + Provider = "openai", + Model = "gpt-test", + InputTokensPrompt = 1000, + OutputTokensGeneration = 500, + }; + + Assert.NotNull(metrics.ContextUsagePercent); + Assert.Equal(0.15, metrics.ContextUsagePercent.Value, 4); + } + + private static void RegisterPricing(string provider, string model, decimal prompt, decimal completion) + { + AIModelCapabilityRegistry.Instance.SetCapabilities(new AIModelCapabilities + { + Provider = provider, + Model = model, + Pricing = new AIModelPricing { Prompt = prompt, Completion = completion }, + }); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AIModels/AICapabilityTests.cs b/src/SmartHopper.ProviderSdk.Tests/AIModels/AICapabilityTests.cs new file mode 100644 index 000000000..69839ef2b --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AIModels/AICapabilityTests.cs @@ -0,0 +1,96 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AIModels +{ + using System; + using SmartHopper.ProviderSdk.AIModels; + using Xunit; + + /// + /// Tests for and its extension methods. + /// + [Collection("ProviderSdk")] + public class AICapabilityTests + { +#if NET7_WINDOWS + private const string PlatformSuffix = " [Windows]"; +#else + private const string PlatformSuffix = " [Core]"; +#endif + + [Theory(DisplayName = nameof(ToDetailedString_FormatsCapabilities) + PlatformSuffix)] + [InlineData(AICapability.None, "None")] + [InlineData(AICapability.Text2Text, "TextInput, TextOutput")] + [InlineData(AICapability.ToolChat, "TextInput, TextOutput, FunctionCalling")] + [InlineData(AICapability.ReasoningChat, "TextInput, TextOutput, Reasoning")] + [InlineData(AICapability.Text2Image, "TextInput, ImageOutput")] + [InlineData(AICapability.AudioInput, "SpeechInput, AudioInput")] + [InlineData(AICapability.AudioOutput, "SpeechOutput, AudioOutput")] + public void ToDetailedString_FormatsCapabilities(AICapability capability, string expected) + { + Assert.Equal(expected, capability.ToDetailedString()); + } + + [Theory(DisplayName = nameof(HasInput_DetectsInputCapabilities) + PlatformSuffix)] + [InlineData(AICapability.None, false)] + [InlineData(AICapability.Text2Text, true)] + [InlineData(AICapability.Text2Image, true)] + [InlineData(AICapability.AudioInput, true)] + [InlineData(AICapability.ImageOutput, false)] + public void HasInput_DetectsInputCapabilities(AICapability capability, bool expected) + { + Assert.Equal(expected, capability.HasInput()); + } + + [Theory(DisplayName = nameof(HasOutput_DetectsOutputCapabilities) + PlatformSuffix)] + [InlineData(AICapability.None, false)] + [InlineData(AICapability.Text2Text, true)] + [InlineData(AICapability.Text2Image, true)] + [InlineData(AICapability.AudioOutput, true)] + [InlineData(AICapability.TextInput, false)] + public void HasOutput_DetectsOutputCapabilities(AICapability capability, bool expected) + { + Assert.Equal(expected, capability.HasOutput()); + } + + [Theory(DisplayName = nameof(HasFlag_DetectsIndividualFlags) + PlatformSuffix)] + [InlineData(AICapability.Text2Text, AICapability.TextInput, true)] + [InlineData(AICapability.Text2Text, AICapability.TextOutput, true)] + [InlineData(AICapability.Text2Text, AICapability.FunctionCalling, false)] + [InlineData(AICapability.ToolChat, AICapability.FunctionCalling, true)] + [InlineData(AICapability.AudioInput, AICapability.SpeechInput, true)] + [InlineData(AICapability.AudioOutput, AICapability.SpeechOutput, true)] + public void HasFlag_DetectsIndividualFlags(AICapability capability, AICapability flag, bool expected) + { + Assert.Equal(expected, capability.HasFlag(flag)); + } + + [Fact(DisplayName = nameof(Text2Text_MatchesTextInputAndTextOutput) + PlatformSuffix)] + public void Text2Text_MatchesTextInputAndTextOutput() + { + Assert.Equal(AICapability.TextInput | AICapability.TextOutput, AICapability.Text2Text); + } + + [Fact(DisplayName = nameof(ToolChat_MatchesText2TextAndFunctionCalling) + PlatformSuffix)] + public void ToolChat_MatchesText2TextAndFunctionCalling() + { + Assert.Equal(AICapability.Text2Text | AICapability.FunctionCalling, AICapability.ToolChat); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AIModels/AIModelCapabilitiesTests.cs b/src/SmartHopper.ProviderSdk.Tests/AIModels/AIModelCapabilitiesTests.cs new file mode 100644 index 000000000..97eee43fc --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AIModels/AIModelCapabilitiesTests.cs @@ -0,0 +1,178 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AIModels +{ + using System.Collections.Generic; + using SmartHopper.ProviderSdk.AIModels; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AIModelCapabilitiesTests + { +#if NET7_WINDOWS + private const string PlatformSuffix = " [Windows]"; +#else + private const string PlatformSuffix = " [Core]"; +#endif + + [Fact(DisplayName = nameof(HasCapability_ReturnsTrueForNone) + PlatformSuffix)] + public void HasCapability_ReturnsTrueForNone() + { + var capabilities = new AIModelCapabilities { Capabilities = AICapability.Text2Text }; + + Assert.True(capabilities.HasCapability(AICapability.None)); + } + + [Fact(DisplayName = nameof(HasCapability_ReturnsTrueForMatchingCapability) + PlatformSuffix)] + public void HasCapability_ReturnsTrueForMatchingCapability() + { + var capabilities = new AIModelCapabilities { Capabilities = AICapability.Text2Text }; + + Assert.True(capabilities.HasCapability(AICapability.Text2Text)); + Assert.True(capabilities.HasCapability(AICapability.TextInput)); + Assert.True(capabilities.HasCapability(AICapability.TextOutput)); + } + + [Fact(DisplayName = nameof(HasCapability_ReturnsFalseForMissingFlag) + PlatformSuffix)] + public void HasCapability_ReturnsFalseForMissingFlag() + { + var capabilities = new AIModelCapabilities { Capabilities = AICapability.Text2Text }; + + Assert.False(capabilities.HasCapability(AICapability.ImageOutput)); + Assert.False(capabilities.HasCapability(AICapability.FunctionCalling)); + } + + [Fact(DisplayName = nameof(GetKey_LowercasesProviderAndModel) + PlatformSuffix)] + public void GetKey_LowercasesProviderAndModel() + { + var capabilities = new AIModelCapabilities + { + Provider = "OpenAI", + Model = "GPT-4", + }; + + Assert.Equal("openai.gpt-4", capabilities.GetKey()); + } + + [Fact(DisplayName = nameof(IsDiscouragedForAnyTool_EmptyListReturnsFalse) + PlatformSuffix)] + public void IsDiscouragedForAnyTool_EmptyListReturnsFalse() + { + var capabilities = new AIModelCapabilities + { + DiscouragedForTools = new List(), + }; + + Assert.False(capabilities.IsDiscouragedForAnyTool(new List { "Tool" })); + } + + [Fact(DisplayName = nameof(IsDiscouragedForAnyTool_WildcardMatchesAll) + PlatformSuffix)] + public void IsDiscouragedForAnyTool_WildcardMatchesAll() + { + var capabilities = new AIModelCapabilities + { + DiscouragedForTools = new List { "*" }, + }; + + Assert.True(capabilities.IsDiscouragedForAnyTool(new List { "AnyTool" })); + } + + [Fact(DisplayName = nameof(IsDiscouragedForAnyTool_MatchesToolName) + PlatformSuffix)] + public void IsDiscouragedForAnyTool_MatchesToolName() + { + var capabilities = new AIModelCapabilities + { + DiscouragedForTools = new List { "MyTool" }, + }; + + Assert.True(capabilities.IsDiscouragedForAnyTool(new List { "MyTool" })); + } + + [Fact(DisplayName = nameof(IsDiscouragedForAnyTool_NoMatchReturnsFalse) + PlatformSuffix)] + public void IsDiscouragedForAnyTool_NoMatchReturnsFalse() + { + var capabilities = new AIModelCapabilities + { + DiscouragedForTools = new List { "OtherTool" }, + }; + + Assert.False(capabilities.IsDiscouragedForAnyTool(new List { "MyTool" })); + } + + [Fact(DisplayName = nameof(IsDiscouragedForAnyTool_MatchesCaseInsensitive) + PlatformSuffix)] + public void IsDiscouragedForAnyTool_MatchesCaseInsensitive() + { + var capabilities = new AIModelCapabilities + { + DiscouragedForTools = new List { "Tool-A" }, + }; + + Assert.True(capabilities.IsDiscouragedForAnyTool(new List { "tool-a" })); + } + + [Fact(DisplayName = nameof(SupportsBatch_DefaultsToNull) + PlatformSuffix)] + public void SupportsBatch_DefaultsToNull() + { + var capabilities = new AIModelCapabilities + { + Provider = "OpenAI", + Model = "gpt-4", + }; + + Assert.Null(capabilities.SupportsBatch); + } + + [Fact(DisplayName = nameof(SupportsBatch_CanBeSetExplicitly) + PlatformSuffix)] + public void SupportsBatch_CanBeSetExplicitly() + { + var capabilities = new AIModelCapabilities + { + Provider = "OpenAI", + Model = "gpt-4", + SupportsBatch = true, + }; + + Assert.True(capabilities.SupportsBatch); + } + + [Fact(DisplayName = nameof(BatchPricing_CanBeSetAndRead) + PlatformSuffix)] + public void BatchPricing_CanBeSetAndRead() + { + var batchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000002m, + }; + + var capabilities = new AIModelCapabilities + { + Provider = "OpenRouter", + Model = "openai/gpt-5", + SupportsBatch = true, + BatchPricing = batchPricing, + }; + + Assert.NotNull(capabilities.BatchPricing); + Assert.Equal(0.000001m, capabilities.BatchPricing.Prompt); + Assert.Equal(0.000002m, capabilities.BatchPricing.Completion); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/AIModels/AIModelCapabilityRegistryTests.cs b/src/SmartHopper.ProviderSdk.Tests/AIModels/AIModelCapabilityRegistryTests.cs new file mode 100644 index 000000000..734692d5e --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/AIModels/AIModelCapabilityRegistryTests.cs @@ -0,0 +1,298 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.AIModels +{ + using System.Collections.Generic; + using SmartHopper.ProviderSdk.AIModels; + using SmartHopper.ProviderSdk.Tests.TestHelpers; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class AIModelCapabilityRegistryTests + { +#if NET7_WINDOWS + [Fact(DisplayName = "SetCapabilities_ThenGetCapabilities_ExactMatch [Windows]")] +#else + [Fact(DisplayName = "SetCapabilities_ThenGetCapabilities_ExactMatch [Core]")] +#endif + public void SetCapabilities_ThenGetCapabilities_ExactMatch() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + Capabilities = AICapability.Text2Text, + }); + + var caps = registry.GetCapabilities("OpenAI", "gpt-4"); + + Assert.NotNull(caps); + Assert.Equal("openai", caps.Provider); + Assert.Equal("gpt-4", caps.Model); + Assert.True(caps.HasCapability(AICapability.Text2Text)); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "GetCapabilities_AliasMatches [Windows]")] +#else + [Fact(DisplayName = "GetCapabilities_AliasMatches [Core]")] +#endif + public void GetCapabilities_AliasMatches() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4-turbo", + Aliases = new List { "gpt-4t" }, + Capabilities = AICapability.Text2Text, + }); + + var caps = registry.GetCapabilities("openai", "gpt-4t"); + + Assert.NotNull(caps); + Assert.Equal("gpt-4-turbo", caps.Model); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "GetCapabilities_UnknownModel_ReturnsNull [Windows]")] +#else + [Fact(DisplayName = "GetCapabilities_UnknownModel_ReturnsNull [Core]")] +#endif + public void GetCapabilities_UnknownModel_ReturnsNull() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + var caps = registry.GetCapabilities("unknown", "unknown"); + + Assert.Null(caps); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "HasProviderCapabilities_DetectsProvider [Windows]")] +#else + [Fact(DisplayName = "HasProviderCapabilities_DetectsProvider [Core]")] +#endif + public void HasProviderCapabilities_DetectsProvider() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + Capabilities = AICapability.Text2Text, + }); + + Assert.True(registry.HasProviderCapabilities("openai")); + Assert.False(registry.HasProviderCapabilities("anthropic")); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "GetProviderModels_ReturnsOnlySameProvider [Windows]")] +#else + [Fact(DisplayName = "GetProviderModels_ReturnsOnlySameProvider [Core]")] +#endif + public void GetProviderModels_ReturnsOnlySameProvider() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + Capabilities = AICapability.Text2Text, + }); + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "anthropic", + Model = "claude-3", + Capabilities = AICapability.Text2Text, + }); + + var openAiModels = registry.GetProviderModels("openai"); + + Assert.Single(openAiModels); + Assert.Equal("gpt-4", openAiModels[0].Model); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "FindModelsWithCapabilities_FiltersByCapability [Windows]")] +#else + [Fact(DisplayName = "FindModelsWithCapabilities_FiltersByCapability [Core]")] +#endif + public void FindModelsWithCapabilities_FiltersByCapability() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "dall-e", + Capabilities = AICapability.Text2Image, + }); + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + Capabilities = AICapability.Text2Text, + }); + + var textModels = registry.FindModelsWithCapabilities(AICapability.Text2Text); + + Assert.Single(textModels); + Assert.Equal("gpt-4", textModels[0].Model); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "SetDefault_Exclusive_RemovesDefaultFromOthers [Windows]")] +#else + [Fact(DisplayName = "SetDefault_Exclusive_RemovesDefaultFromOthers [Core]")] +#endif + public void SetDefault_Exclusive_RemovesDefaultFromOthers() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + Capabilities = AICapability.Text2Text, + Default = AICapability.Text2Text, + }); + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-3.5", + Capabilities = AICapability.Text2Text, + }); + + registry.SetDefault("openai", "gpt-3.5", AICapability.Text2Text, exclusive: true); + + var gpt4 = registry.GetCapabilities("openai", "gpt-4"); + var gpt35 = registry.GetCapabilities("openai", "gpt-3.5"); + + Assert.False((gpt4.Default & AICapability.Text2Text) == AICapability.Text2Text); + Assert.True((gpt35.Default & AICapability.Text2Text) == AICapability.Text2Text); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "GetDefaultModel_ReturnsDefaultForCapability [Windows]")] +#else + [Fact(DisplayName = "GetDefaultModel_ReturnsDefaultForCapability [Core]")] +#endif + public void GetDefaultModel_ReturnsDefaultForCapability() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + Capabilities = AICapability.Text2Text, + Default = AICapability.Text2Text, + Rank = 10, + }); + + var defaultModel = registry.GetDefaultModel("openai", AICapability.Text2Text); + + Assert.Equal("gpt-4", defaultModel); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "SelectBestModel_UnknownUserModel_Allowed [Windows]")] +#else + [Fact(DisplayName = "SelectBestModel_UnknownUserModel_Allowed [Core]")] +#endif + public void SelectBestModel_UnknownUserModel_Allowed() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + Capabilities = AICapability.Text2Text, + }); + + var selected = registry.SelectBestModel("openai", "custom-model", AICapability.Text2Text); + + Assert.Equal("custom-model", selected); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "SelectBestModel_ReturnsDefaultWhenNoUserModel [Windows]")] +#else + [Fact(DisplayName = "SelectBestModel_ReturnsDefaultWhenNoUserModel [Core]")] +#endif + public void SelectBestModel_ReturnsDefaultWhenNoUserModel() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + Capabilities = AICapability.Text2Text, + Default = AICapability.Text2Text, + }); + + var selected = registry.SelectBestModel("openai", string.Empty, AICapability.Text2Text); + + Assert.Equal("gpt-4", selected); + } + +#if NET7_WINDOWS + [Fact(DisplayName = "ModelSupportsStreaming_TrueWhenSupported [Windows]")] +#else + [Fact(DisplayName = "ModelSupportsStreaming_TrueWhenSupported [Core]")] +#endif + public void ModelSupportsStreaming_TrueWhenSupported() + { + ProviderSdkTestHelper.ResetCapabilityRegistry(); + var registry = AIModelCapabilityRegistry.Instance; + + registry.SetCapabilities(new AIModelCapabilities + { + Provider = "openai", + Model = "gpt-4", + SupportsStreaming = true, + }); + + Assert.True(registry.ModelSupportsStreaming("openai", "gpt-4")); + Assert.False(registry.ModelSupportsStreaming("openai", "unknown")); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/Diagnostics/SHRuntimeMessageTests.cs b/src/SmartHopper.ProviderSdk.Tests/Diagnostics/SHRuntimeMessageTests.cs new file mode 100644 index 000000000..7f0e71555 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/Diagnostics/SHRuntimeMessageTests.cs @@ -0,0 +1,90 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.Diagnostics +{ + using SmartHopper.ProviderSdk.Diagnostics; + using Xunit; + + /// + /// Tests for . + /// + [Collection("ProviderSdk")] + public class SHRuntimeMessageTests + { +#if NET7_WINDOWS + private const string PlatformSuffix = " [Windows]"; +#else + private const string PlatformSuffix = " [Core]"; +#endif + + [Fact(DisplayName = nameof(Constructor_StoresAllProperties) + PlatformSuffix)] + public void Constructor_StoresAllProperties() + { + var message = new SHRuntimeMessage( + SHRuntimeMessageSeverity.Warning, + SHRuntimeMessageOrigin.Tool, + SHMessageCode.ToolExecutionError, + "message text", + false); + + Assert.Equal(SHRuntimeMessageSeverity.Warning, message.Severity); + Assert.Equal(SHRuntimeMessageOrigin.Tool, message.Origin); + Assert.Equal(SHMessageCode.ToolExecutionError, message.Code); + Assert.Equal("message text", message.Message); + Assert.False(message.Surfaceable); + } + + [Fact(DisplayName = nameof(Constructor_NullMessageBecomesEmpty) + PlatformSuffix)] + public void Constructor_NullMessageBecomesEmpty() + { + var message = new SHRuntimeMessage( + SHRuntimeMessageSeverity.Info, + SHRuntimeMessageOrigin.Return, + SHMessageCode.Unknown, + null!); + + Assert.Equal(string.Empty, message.Message); + } + + [Fact(DisplayName = nameof(Constructor_SurfaceableDefaultsToTrue) + PlatformSuffix)] + public void Constructor_SurfaceableDefaultsToTrue() + { + var message = new SHRuntimeMessage( + SHRuntimeMessageSeverity.Info, + SHRuntimeMessageOrigin.Return, + SHMessageCode.Unknown, + "message text"); + + Assert.True(message.Surfaceable); + } + + [Fact(DisplayName = nameof(Constructor_SurfaceableCanBeFalse) + PlatformSuffix)] + public void Constructor_SurfaceableCanBeFalse() + { + var message = new SHRuntimeMessage( + SHRuntimeMessageSeverity.Debug, + SHRuntimeMessageOrigin.Return, + SHMessageCode.Unknown, + "message text", + false); + + Assert.False(message.Surfaceable); + } + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/Providers/OpenRouter/OpenRouterBatchProviderTests.cs b/src/SmartHopper.ProviderSdk.Tests/Providers/OpenRouter/OpenRouterBatchProviderTests.cs new file mode 100644 index 000000000..7aaec73a6 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/Providers/OpenRouter/OpenRouterBatchProviderTests.cs @@ -0,0 +1,245 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +#if NET7_WINDOWS + +namespace SmartHopper.ProviderSdk.Tests.Providers.OpenRouter +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Newtonsoft.Json.Linq; + using SmartHopper.Providers.OpenRouter; + using SmartHopper.ProviderSdk.AICall.Batch; + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AIModels; + using Xunit; + + /// + /// Unit tests for the OpenRouter batch provider and the generated model metadata. + /// + [Collection("ProviderSdk")] + public class OpenRouterBatchProviderTests + { + /// + /// The OpenRouter provider instance must implement . + /// + [Fact] + public void Provider_ImplementsBatchInterface() + { + var provider = OpenRouterProvider.Instance; + + Assert.NotNull(provider); + Assert.IsAssignableFrom(provider); + } + + /// + /// The generated OpenRouter model list must not contain any canonical model + /// ids ending with :batch. Those should have been merged into the base + /// model with the SupportsBatch flag. + /// + [Fact] + public async Task Models_NoCanonicalBatchModelIds() + { + var provider = OpenRouterProvider.Instance; + var models = await provider.Models.RetrieveModels().ConfigureAwait(false); + + var batchModelIds = models + .Where(m => m.Model != null && m.Model.EndsWith(":batch", StringComparison.OrdinalIgnoreCase)) + .Select(m => m.Model) + .ToList(); + + Assert.Empty(batchModelIds); + } + + /// + /// A known batch-eligible model should be marked + /// and should expose discounted . + /// + [Fact] + public async Task Models_BatchEligibleModelHasMetadata() + { + var provider = OpenRouterProvider.Instance; + var models = await provider.Models.RetrieveModels().ConfigureAwait(false); + + var model = models.FirstOrDefault(m => + string.Equals(m.Model, "openai/gpt-5.6-luna", StringComparison.OrdinalIgnoreCase)); + + Assert.NotNull(model); + Assert.True(model.SupportsBatch); + Assert.NotNull(model.BatchPricing); + Assert.True(model.BatchPricing.Prompt < model.Pricing.Prompt); + Assert.Contains("openai/gpt-5.6-luna:batch", model.Aliases); + } + + /// + /// Resolving a model through the :batch alias should return the canonical + /// model with batch support. + /// + [Fact] + public async Task Registry_ResolvesBatchAliasToCanonicalModel() + { + var provider = OpenRouterProvider.Instance; + var registry = AIModelCapabilityRegistry.Instance; + var models = await provider.Models.RetrieveModels().ConfigureAwait(false); + + // Ensure provider models are loaded in the registry. + foreach (var model in models) + { + registry.SetCapabilities(model); + } + + var viaAlias = registry.GetCapabilities("OpenRouter", "openai/gpt-5.6-luna:batch"); + var viaBase = registry.GetCapabilities("OpenRouter", "openai/gpt-5.6-luna"); + + Assert.NotNull(viaAlias); + Assert.NotNull(viaBase); + Assert.Same(viaAlias, viaBase); + Assert.True(viaAlias.SupportsBatch); + Assert.NotNull(viaAlias.BatchPricing); + } + + /// + /// must strip the + /// :batch suffix from the model slug so the same model works for both + /// chat and batch endpoints. + /// + [Fact] + public void PreCall_StripsBatchSuffix() + { + var provider = OpenRouterProvider.Instance; + var request = new AIRequestCall + { + Provider = "OpenRouter", + Model = "openai/gpt-5.6-luna:batch", + Capability = SmartHopper.ProviderSdk.AIModels.AICapability.None, + }; + + var prepared = provider.PreCall(request); + + Assert.Equal("openai/gpt-5.6-luna", prepared.Model); + } + + /// + /// Batch result parsing must extract successful response bodies keyed by + /// custom_id. + /// + [Fact] + public void ParseBatchResultsFiles_SuccessResponse_ReturnsBody() + { + var provider = OpenRouterProvider.Instance; + var json = @"[ + { + ""custom_id"": ""sh-20260821000000-req-00-00000000"", + ""response"": { + ""status_code"": 200, + ""body"": { + ""id"": ""chatcmpl-test"", + ""choices"": [ + { ""message"": { ""role"": ""assistant"", ""content"": ""hello"" }, ""finish_reason"": ""stop"" } + ] + } + } + } + ]"; + + var status = provider.ParseBatchResultsFiles(new[] { json }, "batch_123"); + + Assert.Equal(AIBatchState.Completed, status.State); + Assert.Single(status.Results); + Assert.True(status.Results.ContainsKey("sh-20260821000000-req-00-00000000")); + Assert.Equal("chatcmpl-test", status.Results["sh-20260821000000-req-00-00000000"]["id"]?.ToString()); + } + + /// + /// Item-level errors in the results array must surface as provider messages + /// instead of being added to the successful result dictionary. + /// + [Fact] + public void ParseBatchResultsFiles_ItemError_EmitsMessage() + { + var provider = OpenRouterProvider.Instance; + var json = @"[ + { + ""custom_id"": ""sh-20260821000000-req-00-00000000"", + ""error"": { ""message"": ""Batch item rejected"" } + } + ]"; + + var status = provider.ParseBatchResultsFiles(new[] { json }, "batch_123"); + + Assert.Empty(status.Results); + Assert.Single(status.Messages); + Assert.Contains("Batch item rejected", status.Messages[0].Message); + } + + /// + /// Non-2xx HTTP responses at the item level must be reported as errors. + /// + [Fact] + public void ParseBatchResultsFiles_Non2xxResponse_EmitsErrorMessage() + { + var provider = OpenRouterProvider.Instance; + var json = @"[ + { + ""custom_id"": ""sh-20260821000000-req-00-00000000"", + ""response"": { + ""status_code"": 422, + ""body"": { ""error"": { ""message"": ""Unprocessable entity"" } } + } + } + ]"; + + var status = provider.ParseBatchResultsFiles(new[] { json }, "batch_123"); + + Assert.Empty(status.Results); + Assert.Single(status.Messages); + Assert.Contains("Unprocessable entity", status.Messages[0].Message); + } + + /// + /// A full batch object containing a results array must be parsed as well + /// as a raw results array. + /// + [Fact] + public void ParseBatchResultsFiles_BatchObjectWithResults_ReturnsBody() + { + var provider = OpenRouterProvider.Instance; + var json = @"{ + ""id"": ""batch_123"", + ""status"": ""completed"", + ""results"": [ + { + ""custom_id"": ""sh-20260821000000-req-00-00000000"", + ""response"": { + ""status_code"": 200, + ""body"": { ""id"": ""chatcmpl-batch"" } + } + } + ] + }"; + + var status = provider.ParseBatchResultsFiles(new[] { json }, "batch_123"); + + Assert.Equal(AIBatchState.Completed, status.State); + Assert.Single(status.Results); + } + } +} + +#endif diff --git a/src/SmartHopper.ProviderSdk.Tests/SmartHopper.ProviderSdk.Tests.csproj b/src/SmartHopper.ProviderSdk.Tests/SmartHopper.ProviderSdk.Tests.csproj new file mode 100644 index 000000000..a237e3174 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/SmartHopper.ProviderSdk.Tests.csproj @@ -0,0 +1,65 @@ + + + + + net7.0-windows;net7.0 + false + true + NU1701;NETSDK1086 + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + true + $(DefineConstants);WINDOWS;NET7_WINDOWS + + + + + + + + + + + + + + diff --git a/src/SmartHopper.ProviderSdk.Tests/TestHelpers/ProviderSdkTestCollection.cs b/src/SmartHopper.ProviderSdk.Tests/TestHelpers/ProviderSdkTestCollection.cs new file mode 100644 index 000000000..c36ce21b8 --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/TestHelpers/ProviderSdkTestCollection.cs @@ -0,0 +1,34 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.TestHelpers +{ + using System.Diagnostics.CodeAnalysis; + using Xunit; + + /// + /// XUnit collection that disables parallelization across all Provider SDK tests + /// so that global mutable state such as + /// can be reset safely between tests. + /// + [CollectionDefinition("ProviderSdk", DisableParallelization = true)] + [SuppressMessage("Naming", "CA1711", Justification = "Type is an xUnit collection definition.")] + public class ProviderSdkTestCollection : ICollectionFixture + { + } +} diff --git a/src/SmartHopper.ProviderSdk.Tests/TestHelpers/ProviderSdkTestHelper.cs b/src/SmartHopper.ProviderSdk.Tests/TestHelpers/ProviderSdkTestHelper.cs new file mode 100644 index 000000000..0a6bb8c5a --- /dev/null +++ b/src/SmartHopper.ProviderSdk.Tests/TestHelpers/ProviderSdkTestHelper.cs @@ -0,0 +1,37 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Tests.TestHelpers +{ + using SmartHopper.ProviderSdk.AIModels; + + /// + /// Shared helpers for Provider SDK tests. + /// + public static class ProviderSdkTestHelper + { + /// + /// Clears the singleton model dictionary. + /// Call this at the start of tests that register or inspect capabilities. + /// + public static void ResetCapabilityRegistry() + { + AIModelCapabilityRegistry.Instance.Models.Clear(); + } + } +} diff --git a/src/SmartHopper.Infrastructure/AICall/Batch/AIBatchTypes.cs b/src/SmartHopper.ProviderSdk/AICall/Batch/AIBatchTypes.cs similarity index 97% rename from src/SmartHopper.Infrastructure/AICall/Batch/AIBatchTypes.cs rename to src/SmartHopper.ProviderSdk/AICall/Batch/AIBatchTypes.cs index 09b66f7e3..dc3aac677 100644 --- a/src/SmartHopper.Infrastructure/AICall/Batch/AIBatchTypes.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Batch/AIBatchTypes.cs @@ -20,11 +20,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Batch +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Batch { /// /// Possible states of a submitted batch job. @@ -187,7 +186,7 @@ public AIBatchStatus(string batchId, IReadOnlyDictionary result /// files, or multiple user-selected files in manual load) into accumulator /// collections with a first-wins policy on custom_id. /// - internal static class AIBatchStatusMerge + public static class AIBatchStatusMerge { /// /// Merges one parsed single-file into accumulator collections. diff --git a/src/SmartHopper.Infrastructure/AICall/Batch/IAIBatchProvider.cs b/src/SmartHopper.ProviderSdk/AICall/Batch/IAIBatchProvider.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/Batch/IAIBatchProvider.cs rename to src/SmartHopper.ProviderSdk/AICall/Batch/IAIBatchProvider.cs index d6d70612a..9f03bd4f5 100644 --- a/src/SmartHopper.Infrastructure/AICall/Batch/IAIBatchProvider.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Batch/IAIBatchProvider.cs @@ -19,9 +19,8 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Requests; - -namespace SmartHopper.Infrastructure.AICall.Batch +using SmartHopper.ProviderSdk.AICall.Core.Requests; +namespace SmartHopper.ProviderSdk.AICall.Batch { /// /// Optional interface implemented by AI providers that support asynchronous batch processing. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/AIRequestParameters.cs b/src/SmartHopper.ProviderSdk/AICall/Core/AIRequestParameters.cs similarity index 99% rename from src/SmartHopper.Infrastructure/AICall/Core/AIRequestParameters.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/AIRequestParameters.cs index 7b38f686c..8dd0bafd5 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/AIRequestParameters.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/AIRequestParameters.cs @@ -19,8 +19,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using Newtonsoft.Json.Linq; - -namespace SmartHopper.Infrastructure.AICall.Core +namespace SmartHopper.ProviderSdk.AICall.Core { /// /// Immutable per-request AI configuration. Providers read each property individually diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Base/AIAgent.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Base/AIAgent.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/Core/Base/AIAgent.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Base/AIAgent.cs index 298a88e48..a02d508f4 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Base/AIAgent.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Base/AIAgent.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.AICall.Core.Base +namespace SmartHopper.ProviderSdk.AICall.Core.Base { /// /// Specifies the originator of an AI interaction. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Base/AICallStatus.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Base/AICallStatus.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/Core/Base/AICallStatus.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Base/AICallStatus.cs index 47fd441ec..e9f9bb2f9 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Base/AICallStatus.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Base/AICallStatus.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.AICall.Core.Base +namespace SmartHopper.ProviderSdk.AICall.Core.Base { /// /// Specifies the originator of an AI interaction. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Base/AIRequestKind.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Base/AIRequestKind.cs similarity index 95% rename from src/SmartHopper.Infrastructure/AICall/Core/Base/AIRequestKind.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Base/AIRequestKind.cs index 85c9732b5..83ae3e0a2 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Base/AIRequestKind.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Base/AIRequestKind.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.AICall.Core.Base +namespace SmartHopper.ProviderSdk.AICall.Core.Base { /// /// Distinguishes between normal generation requests and provider backoffice/metadata requests. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBody.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBody.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBody.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBody.cs index e2745c045..08e63b0f4 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBody.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBody.cs @@ -19,11 +19,10 @@ using System; using System.Collections.Generic; using System.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Immutable representation of an AI request body. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBodyBuilder.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBodyBuilder.cs similarity index 99% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBodyBuilder.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBodyBuilder.cs index bd7c4b852..cb0a41903 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBodyBuilder.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBodyBuilder.cs @@ -21,12 +21,11 @@ using System.Diagnostics; using System.Linq; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Fluent builder for . Produces immutable instances without diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBodyExtensions.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBodyExtensions.cs similarity index 99% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBodyExtensions.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBodyExtensions.cs index 7352aab98..9c4671c56 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIBodyExtensions.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIBodyExtensions.cs @@ -21,10 +21,9 @@ using System.Diagnostics; using System.Linq; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AIModels; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AIModels; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Extension helpers for immutable AI request bodies. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionAudio.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionAudio.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionAudio.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionAudio.cs index 5eb397d9e..a11f0b84f 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionAudio.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionAudio.cs @@ -17,10 +17,9 @@ */ using System; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Utilities; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Utilities; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Represents an audio interaction for speech-to-text or text-to-speech operations. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionBase.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionBase.cs similarity index 89% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionBase.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionBase.cs index 7bf01c31e..637673efa 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionBase.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionBase.cs @@ -17,10 +17,9 @@ */ using System; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Metrics; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Metrics; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Base class for all interactions, centralizing common properties and defaults. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionImage.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionImage.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionImage.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionImage.cs index 7d0dc5570..19453ffa4 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionImage.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionImage.cs @@ -18,11 +18,10 @@ using System; using System.Collections.Generic; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Represents an AI image interaction — either a generated image result or an image input for vision. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionRuntimeMessage.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionRuntimeMessage.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionRuntimeMessage.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionRuntimeMessage.cs index ee85619b3..7f8ed366e 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionRuntimeMessage.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionRuntimeMessage.cs @@ -16,12 +16,11 @@ * along with this library; if not, see . */ -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Unified UI-only diagnostic interaction carrying a -shaped payload diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionText.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionText.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionText.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionText.cs index 98064c61f..229dc8091 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionText.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionText.cs @@ -17,11 +17,10 @@ */ using System.Globalization; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AICall.Utilities; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AICall.Utilities; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Represents an AI-generated text result with associated metadata. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolCall.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolCall.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolCall.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolCall.cs index 53887d775..475a9c2c6 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolCall.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolCall.cs @@ -18,9 +18,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Represents an AI-generated tool call with associated metadata. @@ -100,7 +99,7 @@ public virtual string GetDedupKey() { var id = !string.IsNullOrEmpty(this.Id) ? this.Id : (this.Name ?? string.Empty); var argsStr = this.Arguments != null ? this.Arguments.ToString(Newtonsoft.Json.Formatting.None) : string.Empty; - var argsHash = !string.IsNullOrEmpty(argsStr) ? SmartHopper.Infrastructure.AICall.Utilities.HashUtility.ComputeShortHash(argsStr) : "none"; + var argsHash = !string.IsNullOrEmpty(argsStr) ? SmartHopper.ProviderSdk.AICall.Utilities.HashUtility.ComputeShortHash(argsStr) : "none"; if (!string.IsNullOrWhiteSpace(this.TurnId)) { diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolCallExtensions.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolCallExtensions.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolCallExtensions.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolCallExtensions.cs index d04f6b9bb..21f52dcfe 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolCallExtensions.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolCallExtensions.cs @@ -17,8 +17,7 @@ */ using Newtonsoft.Json.Linq; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Convenience extensions for safely accessing data. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolResult.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolResult.cs similarity index 95% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolResult.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolResult.cs index de38b7ff4..e613a0942 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/AIInteractionToolResult.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/AIInteractionToolResult.cs @@ -19,11 +19,10 @@ using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Utilities; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Utilities; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Represents an AI-generated tool result with associated metadata. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIInteraction.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIInteraction.cs similarity index 91% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIInteraction.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIInteraction.cs index f743cc158..5dfd72c1a 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIInteraction.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIInteraction.cs @@ -24,10 +24,9 @@ */ using System; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Metrics; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Metrics; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { public interface IAIInteraction { diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIKeyedInteraction.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIKeyedInteraction.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIKeyedInteraction.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIKeyedInteraction.cs index 418eab1c0..a197f3693 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIKeyedInteraction.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIKeyedInteraction.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Provides stable identity keys for chat rendering logic. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIRenderInteraction.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIRenderInteraction.cs similarity index 97% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIRenderInteraction.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIRenderInteraction.cs index 400c8f48f..3332568c0 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/IAIRenderInteraction.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/IAIRenderInteraction.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Defines how an interaction should be rendered into the WebChat HTML. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/SystemPromptBuilder.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/SystemPromptBuilder.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/Core/Interactions/SystemPromptBuilder.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Interactions/SystemPromptBuilder.cs index 7ff69b4cc..4ad5f2196 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Interactions/SystemPromptBuilder.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Interactions/SystemPromptBuilder.cs @@ -20,9 +20,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using SmartHopper.Infrastructure.AIContext; - -namespace SmartHopper.Infrastructure.AICall.Core.Interactions +using SmartHopper.ProviderSdk.Hosting; +namespace SmartHopper.ProviderSdk.AICall.Core.Interactions { /// /// Builder for composing system prompts from multiple sections. @@ -199,7 +198,7 @@ public string Build() var providerFilter = string.Join(" ", this._contextProviderFilters); Debug.WriteLine($"[SystemPromptBuilder] Requesting context with filter: '{providerFilter}'"); - var contextDict = AIContextManager.GetCurrentContext(providerFilter); + var contextDict = ProviderSdkHost.ContextProvider.GetCurrentContext(providerFilter); if (contextDict != null) { diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Requests/AIRequestBase.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Requests/AIRequestBase.cs similarity index 95% rename from src/SmartHopper.Infrastructure/AICall/Core/Requests/AIRequestBase.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Requests/AIRequestBase.cs index 44cdbd296..b7030467c 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Requests/AIRequestBase.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Requests/AIRequestBase.cs @@ -20,14 +20,14 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Requests +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +namespace SmartHopper.ProviderSdk.AICall.Core.Requests { /// /// Represents a fully-specified AI request that providers can execute, including @@ -57,7 +57,7 @@ public class AIRequestBase : IAIRequest public virtual string? Provider { get; set; } /// - public virtual IAIProvider ProviderInstance { get => ProviderManager.Instance.GetProvider(this.Provider); } + public virtual IAIProvider ProviderInstance { get => ProviderSdkHost.ProviderRegistry.GetProvider(this.Provider); } /// public virtual string Model { get => this.GetModelToUse(); set => this.model = value; } @@ -220,7 +220,7 @@ public virtual (bool IsValid, List Errors) IsValid() if (!string.IsNullOrWhiteSpace(provider)) { // 1) Provider-level toggle must allow streaming - var settings = ProviderManager.Instance.GetProviderSettings(provider); + var settings = ProviderSdkHost.ProviderRegistry.GetProviderSettings(provider); if (settings != null && settings.EnableStreaming == false) { messages.Add(new SHRuntimeMessage( @@ -233,7 +233,7 @@ public virtual (bool IsValid, List Errors) IsValid() // 2) Model must support streaming if (!string.IsNullOrWhiteSpace(modelUsed)) { - var supports = ModelManager.Instance.ModelSupportsStreaming(provider, modelUsed); + var supports = AIModelCapabilityRegistry.Instance.ModelSupportsStreaming(provider, modelUsed); if (!supports) { messages.Add(new SHRuntimeMessage( diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Requests/AIRequestCall.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Requests/AIRequestCall.cs similarity index 89% rename from src/SmartHopper.Infrastructure/AICall/Core/Requests/AIRequestCall.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Requests/AIRequestCall.cs index 90991f713..97f44df12 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Requests/AIRequestCall.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Requests/AIRequestCall.cs @@ -24,18 +24,16 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Execution; -using SmartHopper.Infrastructure.AICall.Policies; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Settings; -using SmartHopper.Infrastructure.Streaming; - -namespace SmartHopper.Infrastructure.AICall.Core.Requests +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; +using SmartHopper.ProviderSdk.Streaming; +namespace SmartHopper.ProviderSdk.AICall.Core.Requests { /// /// Represents a fully-specified AI request that providers can execute, including @@ -115,9 +113,10 @@ public override (bool IsValid, List Errors) IsValid() else { // Check for provider integrity verification warning - var effectiveMode = SmartHopperSettings.Instance.EffectiveProviderIntegrityCheckMode; + var trustHost = ProviderSdkHost.ProviderTrust; + var effectiveMode = trustHost.EffectiveIntegrityCheckMode; - if (ProviderManager.Instance.IsProviderMismatched(this.Provider)) + if (trustHost.IsProviderMismatched(this.Provider)) { var integrityMessage = $"Provider '{this.Provider}' failed SHA-256 integrity verification. " + "The provider's hash does not match the official published hash. " + @@ -131,7 +130,7 @@ public override (bool IsValid, List Errors) IsValid() } // Check for unavailable hash verification (network/repository issues) - if (ProviderManager.Instance.IsProviderUnavailable(this.Provider)) + if (trustHost.IsProviderUnavailable(this.Provider)) { // Hash repository was unavailable - add warning var unavailableMessage = $"Provider '{this.Provider}' could not be verified - hash check unavailable due to network issues. " + @@ -145,7 +144,7 @@ public override (bool IsValid, List Errors) IsValid() } // Check for unknown provider (not in hash manifest - custom/third-party) - if (ProviderManager.Instance.IsProviderUnknown(this.Provider)) + if (trustHost.IsProviderUnknown(this.Provider)) { // Provider not found in official hash manifest - add warning var unknownMessage = $"Provider '{this.Provider}' is not known - it may be a custom or third-party provider. " + @@ -158,6 +157,30 @@ public override (bool IsValid, List Errors) IsValid() unknownMessage)); Debug.WriteLine($"[AIRequestCall] Provider '{this.Provider}' is unknown - adding warning"); } + + // Surface community/unsigned warnings on every component using this provider. + if (trustHost.IsProviderCommunity(this.Provider)) + { + var communityMessage = $"Provider '{this.Provider}' is a community provider, not signed by SmartHopper. " + + "Use it only if you trust its source — community providers run with full plugin privileges."; + messages.Add(new SHRuntimeMessage( + SHRuntimeMessageSeverity.Warning, + SHRuntimeMessageOrigin.Validation, + SHMessageCode.UnknownProvider, + communityMessage)); + Debug.WriteLine($"[AIRequestCall] Provider '{this.Provider}' is community - adding warning"); + } + else if (trustHost.IsProviderUnsigned(this.Provider)) + { + var unsignedMessage = $"Provider '{this.Provider}' is unsigned. " + + "Use it only if you trust its source."; + messages.Add(new SHRuntimeMessage( + SHRuntimeMessageSeverity.Warning, + SHRuntimeMessageOrigin.Validation, + SHMessageCode.UnknownProvider, + unsignedMessage)); + Debug.WriteLine($"[AIRequestCall] Provider '{this.Provider}' is unsigned - adding warning"); + } } if (this.ProviderInstance == null) @@ -199,7 +222,7 @@ public override (bool IsValid, List Errors) IsValid() var requestedModel = this.RequestedModel; if (!string.IsNullOrWhiteSpace(this.Provider) && !string.IsNullOrWhiteSpace(requestedModel)) { - var requestedCaps = ModelManager.Instance.GetCapabilities(this.Provider, requestedModel); + var requestedCaps = AIModelCapabilityRegistry.Instance.GetCapabilities(this.Provider, requestedModel); if (requestedCaps == null) { messages.Add(new SHRuntimeMessage( @@ -308,7 +331,7 @@ public virtual async Task Exec(bool stream = false, System.Threading.C cancellationToken.ThrowIfCancellationRequested(); // Always-on: run request policies before validation/provider call - await PolicyPipeline.Default.ApplyRequestPoliciesAsync(this).ConfigureAwait(false); + await ProviderSdkHost.PolicyPipeline.ApplyRequestPoliciesAsync(this).ConfigureAwait(false); // Validate early to avoid provider calls when request is invalid var (rqOk, rqErrors) = this.IsValid(); @@ -366,7 +389,7 @@ public virtual async Task Exec(bool stream = false, System.Threading.C /* logging only */ } - await PolicyPipeline.Default.ApplyResponsePoliciesAsync(air).ConfigureAwait(false); + await ProviderSdkHost.PolicyPipeline.ApplyResponsePoliciesAsync(air).ConfigureAwait(false); try { Debug.WriteLine($"[AIRequest.Exec] after policies: interactions={air?.Body?.InteractionsCount ?? 0}, new={string.Join(",", air?.Body?.InteractionsNew ?? new System.Collections.Generic.List())}"); @@ -431,9 +454,16 @@ public virtual async Task Exec(bool stream = false, System.Threading.C /// Aggregated AIReturn from the streaming results. private async Task ExecStreamingInternal(System.Threading.CancellationToken cancellationToken = default) { - // Get the streaming adapter from the provider - var executor = new DefaultProviderExecutor(); - var adapter = executor.TryGetStreamingAdapter(this); + // Get the streaming adapter directly from the provider instance. + IStreamingAdapter adapter = null; + try + { + adapter = this.ProviderInstance?.GetStreamingAdapter(); + } + catch (Exception ex) + { + Debug.WriteLine($"[AIRequestCall.ExecStreamingInternal] Error getting streaming adapter: {ex.Message}"); + } if (adapter == null) { diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Requests/IAIRequest.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Requests/IAIRequest.cs similarity index 89% rename from src/SmartHopper.Infrastructure/AICall/Core/Requests/IAIRequest.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Requests/IAIRequest.cs index b950f1f2e..8eaa4d5f5 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Requests/IAIRequest.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Requests/IAIRequest.cs @@ -18,14 +18,13 @@ using System.Collections.Generic; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Requests +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Core.Requests { public interface IAIRequest { diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Returns/AIReturn.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Returns/AIReturn.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/Core/Returns/AIReturn.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Returns/AIReturn.cs index 17804ad16..8ebee5927 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Returns/AIReturn.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Returns/AIReturn.cs @@ -21,13 +21,12 @@ using System.Diagnostics; using System.Linq; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Returns +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Core.Returns { /// /// Generic container for provider responses, carrying processed and raw results, diff --git a/src/SmartHopper.Infrastructure/AICall/Core/Returns/IAIReturn.cs b/src/SmartHopper.ProviderSdk/AICall/Core/Returns/IAIReturn.cs similarity index 93% rename from src/SmartHopper.Infrastructure/AICall/Core/Returns/IAIReturn.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/Returns/IAIReturn.cs index 950d76780..ba4d89ec0 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/Returns/IAIReturn.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/Returns/IAIReturn.cs @@ -17,13 +17,12 @@ */ using System.Collections.Generic; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Core.Returns +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Core.Returns { /// /// Generic result type for AI evaluations, providing a standard interface between tools and components. diff --git a/src/SmartHopper.Infrastructure/AICall/Core/TimeoutDefaults.cs b/src/SmartHopper.ProviderSdk/AICall/Core/TimeoutDefaults.cs similarity index 97% rename from src/SmartHopper.Infrastructure/AICall/Core/TimeoutDefaults.cs rename to src/SmartHopper.ProviderSdk/AICall/Core/TimeoutDefaults.cs index 2466f1e71..e539bc1c3 100644 --- a/src/SmartHopper.Infrastructure/AICall/Core/TimeoutDefaults.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Core/TimeoutDefaults.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.AICall.Core +namespace SmartHopper.ProviderSdk.AICall.Core { /// /// Centralized timeout defaults and bounds for AI request, tool, and provider HTTP layers. diff --git a/src/SmartHopper.Infrastructure/AICall/JsonSchemas/IJsonSchemaService.cs b/src/SmartHopper.ProviderSdk/AICall/JsonSchemas/IJsonSchemaService.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/JsonSchemas/IJsonSchemaService.cs rename to src/SmartHopper.ProviderSdk/AICall/JsonSchemas/IJsonSchemaService.cs index 0dddcad47..13f188dfd 100644 --- a/src/SmartHopper.Infrastructure/AICall/JsonSchemas/IJsonSchemaService.cs +++ b/src/SmartHopper.ProviderSdk/AICall/JsonSchemas/IJsonSchemaService.cs @@ -17,8 +17,7 @@ */ using Newtonsoft.Json.Linq; - -namespace SmartHopper.Infrastructure.AICall.JsonSchemas +namespace SmartHopper.ProviderSdk.AICall.JsonSchemas { /// /// Centralized service for handling JSON schema wrapping, unwrapping and validation. diff --git a/src/SmartHopper.Infrastructure/AICall/JsonSchemas/JsonSchemaService.cs b/src/SmartHopper.ProviderSdk/AICall/JsonSchemas/JsonSchemaService.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/JsonSchemas/JsonSchemaService.cs rename to src/SmartHopper.ProviderSdk/AICall/JsonSchemas/JsonSchemaService.cs index 276d793fd..1d61d283c 100644 --- a/src/SmartHopper.Infrastructure/AICall/JsonSchemas/JsonSchemaService.cs +++ b/src/SmartHopper.ProviderSdk/AICall/JsonSchemas/JsonSchemaService.cs @@ -19,8 +19,7 @@ using System; using System.Threading; using Newtonsoft.Json.Linq; - -namespace SmartHopper.Infrastructure.AICall.JsonSchemas +namespace SmartHopper.ProviderSdk.AICall.JsonSchemas { /// /// Default implementation of IJsonSchemaService. diff --git a/src/SmartHopper.Infrastructure/AICall/JsonSchemas/ProviderSchemaAdapters.cs b/src/SmartHopper.ProviderSdk/AICall/JsonSchemas/ProviderSchemaAdapters.cs similarity index 99% rename from src/SmartHopper.Infrastructure/AICall/JsonSchemas/ProviderSchemaAdapters.cs rename to src/SmartHopper.ProviderSdk/AICall/JsonSchemas/ProviderSchemaAdapters.cs index 13db227e2..d05a15634 100644 --- a/src/SmartHopper.Infrastructure/AICall/JsonSchemas/ProviderSchemaAdapters.cs +++ b/src/SmartHopper.ProviderSdk/AICall/JsonSchemas/ProviderSchemaAdapters.cs @@ -19,8 +19,7 @@ using System; using System.Collections.Concurrent; using Newtonsoft.Json.Linq; - -namespace SmartHopper.Infrastructure.AICall.JsonSchemas +namespace SmartHopper.ProviderSdk.AICall.JsonSchemas { /// /// Adapter contract that allows each provider project to define how diff --git a/src/SmartHopper.ProviderSdk/AICall/Metrics/AICostCalculator.cs b/src/SmartHopper.ProviderSdk/AICall/Metrics/AICostCalculator.cs new file mode 100644 index 000000000..92c1ba4a1 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/AICall/Metrics/AICostCalculator.cs @@ -0,0 +1,90 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using SmartHopper.ProviderSdk.AIModels; + +namespace SmartHopper.ProviderSdk.AICall.Metrics +{ + /// + /// Centralized, provider-agnostic cost estimator for . + /// Uses per-model pricing from and the + /// token buckets tracked by . + /// + public static class AICostCalculator + { + /// + /// Calculates an estimated cost in USD for the supplied metrics. + /// + /// The metrics to estimate cost for. + /// Estimated cost in USD. Returns 0 when pricing is unknown, free, negative, or no tokens are present. + public static decimal Calculate(AIMetrics metrics) + { + if (metrics == null || + string.IsNullOrEmpty(metrics.Provider) || + string.IsNullOrEmpty(metrics.Model)) + { + return 0m; + } + + var capabilities = AIModelCapabilityRegistry.Instance?.GetCapabilities(metrics.Provider, metrics.Model); + var pricing = capabilities?.Pricing; + if (pricing == null) + { + return 0m; + } + + decimal cost = 0m; + + bool hasActualInput = metrics.InputTokens > 0; + if (hasActualInput) + { + cost += metrics.InputTokensPrompt * GetPositivePrice(pricing.Prompt); + cost += metrics.InputTokensCached * GetPositivePrice(pricing.InputCacheRead); + cost += metrics.InputTokensCacheWrite * GetPositivePrice(pricing.InputCacheWrite); + } + else if (metrics.EstimatedInputTokens > 0) + { + cost += metrics.EstimatedInputTokens * GetPositivePrice(pricing.Prompt); + } + + bool hasActualOutput = metrics.OutputTokens > 0; + if (hasActualOutput) + { + cost += metrics.OutputTokensGeneration * GetPositivePrice(pricing.Completion); + cost += metrics.OutputTokensReasoning * GetPositivePrice(pricing.InternalReasoning); + } + else if (metrics.EstimatedOutputTokens > 0) + { + cost += metrics.EstimatedOutputTokens * GetPositivePrice(pricing.Completion); + } + + return cost; + } + + /// + /// Returns the price when it is a positive value; otherwise returns 0. + /// Negative or missing prices are treated as zero cost for that category. + /// + /// The optional price. + /// A non-negative price value. + private static decimal GetPositivePrice(decimal? price) + { + return price.HasValue && price.Value > 0m ? price.Value : 0m; + } + } +} diff --git a/src/SmartHopper.Infrastructure/AICall/Metrics/AIMetrics.cs b/src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetrics.cs similarity index 91% rename from src/SmartHopper.Infrastructure/AICall/Metrics/AIMetrics.cs rename to src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetrics.cs index 423d651db..b89e17b03 100644 --- a/src/SmartHopper.Infrastructure/AICall/Metrics/AIMetrics.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetrics.cs @@ -21,11 +21,10 @@ using System.Diagnostics; using System.Linq; using Newtonsoft.Json; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Metrics +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Metrics { public class AIMetrics { @@ -137,6 +136,29 @@ public class AIMetrics public int LastEffectiveTotalTokens { get; set; } + private decimal? _estimatedCost; + + /// + /// Gets the estimated cost of the AI call in USD. + /// Calculated using provider/model pricing from + /// and the token buckets tracked by this metrics instance. + /// Returns 0 when pricing is unknown, free, negative, or no tokens are present. + /// + [JsonProperty("estimated_cost")] + public decimal EstimatedCost + { + get + { + if (!this._estimatedCost.HasValue) + { + this._estimatedCost = AICostCalculator.Calculate(this); + } + + return this._estimatedCost.Value; + } + private set => this._estimatedCost = value; + } + /// /// Gets the context usage percentage (0.0 to 1.0) based on EffectiveTotalTokens and the model's context limit. /// Calculated using provider/model from this metrics instance. @@ -152,7 +174,7 @@ public double? ContextUsagePercent return null; } - var modelManager = ModelManager.Instance; + var modelManager = AIModelCapabilityRegistry.Instance; var capabilities = modelManager?.GetCapabilities(this.Provider, this.Model); var contextLimit = capabilities?.ContextLimit; @@ -238,9 +260,11 @@ public void Combine(AIMetrics other) if (!skipLog) { - Debug.WriteLine($"[AIMetrics] Combining metrics:\nProvider: {this.Provider} -> {other.Provider}\nModel: {this.Model} -> {other.Model}\nInputTokensPrompt: {this.InputTokensPrompt} -> {this.InputTokensPrompt + other.InputTokensPrompt}\nInputTokensCached: {this.InputTokensCached} -> {this.InputTokensCached + other.InputTokensCached}\nInputTokensCacheWrite: {this.InputTokensCacheWrite} -> {this.InputTokensCacheWrite + other.InputTokensCacheWrite}\nOutputTokensReasoning: {this.OutputTokensReasoning} -> {this.OutputTokensReasoning + other.OutputTokensReasoning}\nOutputTokensGeneration: {this.OutputTokensGeneration} -> {this.OutputTokensGeneration + other.OutputTokensGeneration}\nEstimatedInputTokens: {this.EstimatedInputTokens} -> {this.EstimatedInputTokens + other.EstimatedInputTokens}\nEstimatedOutputTokens: {this.EstimatedOutputTokens} -> {this.EstimatedOutputTokens + other.EstimatedOutputTokens}\nCompletionTime: {this.CompletionTime} -> {this.CompletionTime + other.CompletionTime}\nFinishReason: {this.FinishReason} -> {other.FinishReason}"); + Debug.WriteLine($"[AIMetrics] Combining metrics:\nProvider: {this.Provider} -> {other.Provider}\nModel: {this.Model} -> {other.Model}\nInputTokensPrompt: {this.InputTokensPrompt} -> {this.InputTokensPrompt + other.InputTokensPrompt}\nInputTokensCached: {this.InputTokensCached} -> {this.InputTokensCached + other.InputTokensCached}\nInputTokensCacheWrite: {this.InputTokensCacheWrite} -> {this.InputTokensCacheWrite + other.InputTokensCacheWrite}\nOutputTokensReasoning: {this.OutputTokensReasoning} -> {this.OutputTokensReasoning + other.OutputTokensReasoning}\nOutputTokensGeneration: {this.OutputTokensGeneration} -> {this.OutputTokensGeneration + other.OutputTokensGeneration}\nEstimatedInputTokens: {this.EstimatedInputTokens} -> {this.EstimatedInputTokens + other.EstimatedInputTokens}\nEstimatedOutputTokens: {this.EstimatedOutputTokens} -> {this.EstimatedOutputTokens + other.EstimatedOutputTokens}\nCompletionTime: {this.CompletionTime} -> {this.CompletionTime + other.CompletionTime}\nEstimatedCost: {this.EstimatedCost} -> {this.EstimatedCost + other.EstimatedCost}\nFinishReason: {this.FinishReason} -> {other.FinishReason}"); } + this.EstimatedCost += other.EstimatedCost; + this.Provider = CombineCommaSeparated(this.Provider, other.Provider, "Unknown"); this.Model = CombineCommaSeparated(this.Model, other.Model); this.Role = CombineCommaSeparated(this.Role, other.Role); diff --git a/src/SmartHopper.Infrastructure/AICall/Metrics/AIMetricsDomain.cs b/src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetricsDomain.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AICall/Metrics/AIMetricsDomain.cs rename to src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetricsDomain.cs index 9e4ac7c17..0671ab86d 100644 --- a/src/SmartHopper.Infrastructure/AICall/Metrics/AIMetricsDomain.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetricsDomain.cs @@ -19,8 +19,7 @@ #nullable enable using System; - -namespace SmartHopper.Infrastructure.AICall.Metrics +namespace SmartHopper.ProviderSdk.AICall.Metrics { /// /// Correlation keys used to associate metrics across session/turn/request/tool boundaries. diff --git a/src/SmartHopper.Infrastructure/AICall/Metrics/AIMetricsList.cs b/src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetricsList.cs similarity index 97% rename from src/SmartHopper.Infrastructure/AICall/Metrics/AIMetricsList.cs rename to src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetricsList.cs index 32001d605..ca1adca6d 100644 --- a/src/SmartHopper.Infrastructure/AICall/Metrics/AIMetricsList.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Metrics/AIMetricsList.cs @@ -19,8 +19,7 @@ using System; using System.Collections.Generic; using System.Linq; - -namespace SmartHopper.Infrastructure.AICall.Metrics +namespace SmartHopper.ProviderSdk.AICall.Metrics { /// /// Ordered list of per-call metrics for a single component solve. diff --git a/src/SmartHopper.Infrastructure/AICall/Utilities/HashUtility.cs b/src/SmartHopper.ProviderSdk/AICall/Utilities/HashUtility.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AICall/Utilities/HashUtility.cs rename to src/SmartHopper.ProviderSdk/AICall/Utilities/HashUtility.cs index b296e811e..5fc0da999 100644 --- a/src/SmartHopper.Infrastructure/AICall/Utilities/HashUtility.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Utilities/HashUtility.cs @@ -19,8 +19,7 @@ using System; using System.Security.Cryptography; using System.Text; - -namespace SmartHopper.Infrastructure.AICall.Utilities +namespace SmartHopper.ProviderSdk.AICall.Utilities { /// /// Shared hashing utilities for generating consistent identifiers. diff --git a/src/SmartHopper.Infrastructure/AICall/Utilities/InteractionUtility.cs b/src/SmartHopper.ProviderSdk/AICall/Utilities/InteractionUtility.cs similarity index 94% rename from src/SmartHopper.Infrastructure/AICall/Utilities/InteractionUtility.cs rename to src/SmartHopper.ProviderSdk/AICall/Utilities/InteractionUtility.cs index 9f69e5541..7cc26c75b 100644 --- a/src/SmartHopper.Infrastructure/AICall/Utilities/InteractionUtility.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Utilities/InteractionUtility.cs @@ -18,9 +18,8 @@ using System; using System.Collections.Generic; -using SmartHopper.Infrastructure.AICall.Core.Interactions; - -namespace SmartHopper.Infrastructure.AICall.Utilities +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +namespace SmartHopper.ProviderSdk.AICall.Utilities { /// /// Shared utilities for working with AI interactions. diff --git a/src/SmartHopper.Infrastructure/AICall/Utilities/RuntimeMessageUtility.cs b/src/SmartHopper.ProviderSdk/AICall/Utilities/RuntimeMessageUtility.cs similarity index 82% rename from src/SmartHopper.Infrastructure/AICall/Utilities/RuntimeMessageUtility.cs rename to src/SmartHopper.ProviderSdk/AICall/Utilities/RuntimeMessageUtility.cs index 551371e0b..75650eecd 100644 --- a/src/SmartHopper.Infrastructure/AICall/Utilities/RuntimeMessageUtility.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Utilities/RuntimeMessageUtility.cs @@ -19,10 +19,8 @@ using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Tools; -using SmartHopper.Infrastructure.Diagnostics; - -namespace SmartHopper.Infrastructure.AICall.Utilities +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.AICall.Utilities { /// /// Shared utilities for working with SHRuntimeMessage collections. @@ -104,36 +102,6 @@ public static List ExtractMessages(JObject toolResult) return messages; } - /// - /// Extracts runtime messages from a envelope. - /// Combines diagnostics carried on the envelope with messages found inside - /// the underlying JSON payload. - /// - /// The envelope to extract messages from. - /// A list of extracted SHRuntimeMessage objects, or empty list if none found. - public static List ExtractMessages(ToolCallResult toolResult) - { - var messages = new List(); - if (toolResult == null) - { - return messages; - } - - if (toolResult.Messages != null) - { - foreach (var m in toolResult.Messages) - { - if (m != null) - { - messages.Add(m); - } - } - } - - messages.AddRange(ExtractMessages(toolResult.Result)); - return messages; - } - /// /// Helper to extract messages from a JArray. /// diff --git a/src/SmartHopper.Infrastructure/AICall/Utilities/TextStreamCoalescer.cs b/src/SmartHopper.ProviderSdk/AICall/Utilities/TextStreamCoalescer.cs similarity index 97% rename from src/SmartHopper.Infrastructure/AICall/Utilities/TextStreamCoalescer.cs rename to src/SmartHopper.ProviderSdk/AICall/Utilities/TextStreamCoalescer.cs index cc226144f..9282cc139 100644 --- a/src/SmartHopper.Infrastructure/AICall/Utilities/TextStreamCoalescer.cs +++ b/src/SmartHopper.ProviderSdk/AICall/Utilities/TextStreamCoalescer.cs @@ -17,9 +17,8 @@ */ using System; -using SmartHopper.Infrastructure.AICall.Core.Interactions; - -namespace SmartHopper.Infrastructure.AICall.Utilities +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +namespace SmartHopper.ProviderSdk.AICall.Utilities { /// /// Shared utility for coalescing streaming text deltas. diff --git a/src/SmartHopper.Infrastructure/AIModels/AICapability.cs b/src/SmartHopper.ProviderSdk/AIModels/AICapability.cs similarity index 99% rename from src/SmartHopper.Infrastructure/AIModels/AICapability.cs rename to src/SmartHopper.ProviderSdk/AIModels/AICapability.cs index 67d3fb3ae..124241285 100644 --- a/src/SmartHopper.Infrastructure/AIModels/AICapability.cs +++ b/src/SmartHopper.ProviderSdk/AIModels/AICapability.cs @@ -18,8 +18,7 @@ using System; using System.Collections.Generic; - -namespace SmartHopper.Infrastructure.AIModels +namespace SmartHopper.ProviderSdk.AIModels { /// /// Defines the capabilities that an AI model can support. diff --git a/src/SmartHopper.Infrastructure/AIModels/AIModelCapabilities.cs b/src/SmartHopper.ProviderSdk/AIModels/AIModelCapabilities.cs similarity index 87% rename from src/SmartHopper.Infrastructure/AIModels/AIModelCapabilities.cs rename to src/SmartHopper.ProviderSdk/AIModels/AIModelCapabilities.cs index 5a1d182c2..a4ac2f481 100644 --- a/src/SmartHopper.Infrastructure/AIModels/AIModelCapabilities.cs +++ b/src/SmartHopper.ProviderSdk/AIModels/AIModelCapabilities.cs @@ -19,8 +19,7 @@ using System; using System.Collections.Generic; using System.Linq; - -namespace SmartHopper.Infrastructure.AIModels +namespace SmartHopper.ProviderSdk.AIModels { /// /// Represents the capabilities and metadata of a specific AI model. @@ -64,6 +63,15 @@ public class AIModelCapabilities /// public bool SupportsPromptCaching { get; set; } + /// + /// Whether this model supports asynchronous batch processing. + /// + /// null (default/omitted) means unknown: allow batch if the provider implements + /// . true means explicitly supported. + /// false means explicitly unsupported and the component will fall back to synchronous. + /// + public bool? SupportsBatch { get; set; } + /// /// Optional ranking to break ties when multiple models match; higher is preferred. /// @@ -102,6 +110,13 @@ public class AIModelCapabilities /// public AIModelPricing Pricing { get; set; } + /// + /// Pricing information for asynchronous batch processing, if the provider publishes + /// discounted batch rates (e.g. OpenRouter's :batch variant). + /// When null, standard is used for batch cost estimates. + /// + public AIModelPricing BatchPricing { get; set; } + /// /// List of AI tool names for which this model is discouraged. /// When a component uses any of these tools, a "not recommended" badge will be displayed. diff --git a/src/SmartHopper.ProviderSdk/AIModels/AIModelCapabilityRegistry.cs b/src/SmartHopper.ProviderSdk/AIModels/AIModelCapabilityRegistry.cs new file mode 100644 index 000000000..d06eccb29 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/AIModels/AIModelCapabilityRegistry.cs @@ -0,0 +1,377 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +namespace SmartHopper.ProviderSdk.AIModels +{ + /// + /// Singleton registry of AI model capabilities keyed by "provider.model". + /// Lives in the Provider SDK so providers can register/query capabilities without + /// depending on the SmartHopper host. Thread-safe via ConcurrentDictionary and a + /// coarse-grained lock around mutating operations that span multiple models + /// (e.g. setting an exclusive default). + /// + public sealed class AIModelCapabilityRegistry + { + private static readonly Lazy _instance = + new Lazy(() => new AIModelCapabilityRegistry()); + + private readonly object _syncRoot = new object(); + + /// + /// Gets the process-wide singleton instance of the capability registry. + /// + public static AIModelCapabilityRegistry Instance => _instance.Value; + + /// + /// Initializes a new instance. Prefer ; the public + /// constructor exists primarily for unit-test scenarios. + /// + public AIModelCapabilityRegistry() + { + } + + /// + /// Gets the dictionary of model capabilities keyed by "provider.model". + /// Thread-safe: uses ConcurrentDictionary for reads/writes. + /// + public ConcurrentDictionary Models { get; } = new ConcurrentDictionary(); + + #region Capability lookup + + /// + /// Gets capabilities for a specific model. + /// Enforces exact name or alias matching only. Wildcard patterns are not supported. + /// + /// The provider name. + /// The model name. + /// Model capabilities or null if not found. + public AIModelCapabilities GetCapabilities(string provider, string model) + { + var key = $"{provider?.ToLowerInvariant()}.{model?.ToLowerInvariant()}"; + + // Try exact match first (fastest path) + if (this.Models.TryGetValue(key, out var capabilities)) + { + return capabilities; + } + + // Fallback: search by alias within the same provider + var providerLower = provider?.ToLowerInvariant(); + var modelLower = model?.ToLowerInvariant(); + var byAlias = this.Models.Values + .Where(m => m != null && m.Provider.Equals(providerLower, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(m => m.Aliases != null && m.Aliases.Any(a => string.Equals(a, modelLower, StringComparison.OrdinalIgnoreCase))); + + return byAlias; + } + + /// + /// Returns true when at least one model is registered for the specified provider. + /// + public bool HasProviderCapabilities(string provider) + { + if (string.IsNullOrWhiteSpace(provider)) + { + return false; + } + + return this.Models.Keys.Any(key => key.StartsWith($"{provider}.", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Returns whether the specified provider/model pair supports provider-side streaming. + /// Unknown models are treated as not supporting streaming (safe fallback). + /// + public bool ModelSupportsStreaming(string provider, string model) + { + if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(model)) + { + return false; + } + + var caps = this.GetCapabilities(provider, model); + return caps != null && caps.SupportsStreaming; + } + + #endregion + + #region Capability registration + + /// + /// Adds or updates capabilities for a specific model. + /// + public void SetCapabilities(AIModelCapabilities capabilities) + { + if (capabilities == null) return; + + var key = capabilities.GetKey(); + lock (this._syncRoot) + { + this.Models[key] = capabilities; + } + } + + /// + /// Convenience helper to register capabilities for a single (provider, model) pair. + /// + public void RegisterCapabilities( + string provider, + string modelName, + AICapability capabilities, + AICapability defaultFor = AICapability.None) + { + if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(modelName)) + { + return; + } + + this.SetCapabilities(new AIModelCapabilities + { + Provider = provider.ToLowerInvariant(), + Model = modelName, + Capabilities = capabilities, + Default = defaultFor, + }); + } + + /// + /// Marks a model as the default for the given capabilities. When exclusive is true (default), + /// clears those capabilities from Default on other models of the same provider so that there is + /// at most one default per capability. + /// + public void SetDefault(string provider, string model, AICapability caps, bool exclusive = true) + { + if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(model) || caps == AICapability.None) + { + return; + } + + var normalizedProvider = provider.ToLowerInvariant(); + + lock (this._syncRoot) + { + if (exclusive) + { + foreach (var m in this.GetProviderModels(normalizedProvider)) + { + if (!string.Equals(m.Model, model, StringComparison.Ordinal)) + { + var before = m.Default; + m.Default &= ~caps; + if (m.Default != before) + { + this.Models[m.GetKey()] = m; + } + } + } + } + + var target = this.GetCapabilities(normalizedProvider, model) ?? new AIModelCapabilities + { + Provider = normalizedProvider, + Model = model, + Capabilities = AICapability.None, + Default = AICapability.None, + }; + + target.Default |= caps; + this.Models[target.GetKey()] = target; + } + } + + #endregion + + #region Model listing and selection + + /// + /// Returns all registered models for a provider. + /// + public List GetProviderModels(string provider) + { + if (string.IsNullOrWhiteSpace(provider)) + { + return new List(); + } + + return this.Models.Values + .Where(m => m != null && string.Equals(m.Provider, provider, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + /// + /// Gets all models that support the specified capabilities. + /// + public List FindModelsWithCapabilities(AICapability requiredCapabilities) + { + if (requiredCapabilities == AICapability.None) + { + return this.Models.Values.ToList(); + } + + return this.Models.Values + .Where(model => model.HasCapability(requiredCapabilities)) + .ToList(); + } + + /// + /// Gets the default model for a provider and specific capability. + /// + public string GetDefaultModel(string provider, AICapability requiredCapability = AICapability.Text2Text) + { + if (string.IsNullOrWhiteSpace(provider)) + { + return string.Empty; + } + + var providerModels = this.GetProviderModels(provider); + if (providerModels.Count == 0) + { + return string.Empty; + } + + var exact = providerModels + .Where(m => (m.Default & requiredCapability) == requiredCapability) + .OrderByDescending(m => m.Verified) + .ThenByDescending(m => m.Rank) + .ThenBy(m => m.Deprecated) + .ThenBy(m => m.Model, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); + if (exact != null) + { + return exact.Model; + } + + var compatible = providerModels + .Where(m => m.Default != AICapability.None && m.HasCapability(requiredCapability)) + .OrderByDescending(m => m.Verified) + .ThenByDescending(m => m.Rank) + .ThenBy(m => m.Deprecated) + .ThenBy(m => m.Model, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); + return compatible?.Model ?? string.Empty; + } + + /// + /// DO NOT CALL THIS DIRECTLY. USE AIProvider.SelectModel() INSTEAD. + /// Centralized model selection and fallback logic. + /// + public string SelectBestModel(string provider, string userModel, AICapability requiredCapability, string preferredDefault = null) + { + if (string.IsNullOrWhiteSpace(provider) || requiredCapability == AICapability.None) + { + Debug.WriteLine($"[AIModelCapabilityRegistry.SelectBestModel] Invalid args -> provider='{provider}', required='{requiredCapability.ToDetailedString()}'"); + return string.Empty; + } + + // If user specified a model, validate capability if known; allow unknown to pass + if (!string.IsNullOrWhiteSpace(userModel)) + { + var known = this.GetCapabilities(provider, userModel); + if (known == null) + { + return userModel; + } + + if (known.HasCapability(requiredCapability)) + { + return userModel; + } + } + + var candidates = this.GetProviderModels(provider) + .Where(m => m.HasCapability(requiredCapability)) + .ToList(); + + if (!string.IsNullOrWhiteSpace(preferredDefault)) + { + var preferredCaps = this.GetCapabilities(provider, preferredDefault); + if (preferredCaps != null && preferredCaps.HasCapability(requiredCapability)) + { + return preferredDefault; + } + } + + if (candidates.Count == 0) + { + return string.Empty; + } + + // 1) Defaults with exact capability + var defaultExact = candidates + .Where(m => (m.Default & requiredCapability) == requiredCapability) + .OrderByDescending(m => m.Verified) + .ThenByDescending(m => m.Rank) + .ThenBy(m => m.Deprecated) + .ThenBy(m => m.Model, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); + if (defaultExact != null) + { + return defaultExact.Model; + } + + // 2) Other defaults that are compatible + var defaultCompatible = candidates + .Where(m => m.Default != AICapability.None) + .OrderByDescending(m => m.Verified) + .ThenByDescending(m => m.Rank) + .ThenBy(m => m.Deprecated) + .ThenBy(m => m.Model, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); + if (defaultCompatible != null) + { + return defaultCompatible.Model; + } + + // 3) Any other registered model, ordered by quality + var best = candidates + .OrderByDescending(m => m.Verified) + .ThenByDescending(m => m.Rank) + .ThenBy(m => m.Deprecated) + .ThenBy(m => m.Model, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); + return best?.Model ?? string.Empty; + } + + #endregion + + #region Capability validation + + /// + /// Validates whether a model registered for the given provider has the required capabilities. + /// Unknown (unregistered) models bypass validation and are treated as compatible — the request + /// layer is responsible for additional checks. + /// + public bool ValidateCapabilities(string provider, string model, AICapability requiredCapability) + { + var capabilities = this.GetCapabilities(provider, model); + if (capabilities == null) + { + return true; + } + + return capabilities.HasCapability(requiredCapability); + } + + #endregion + } +} diff --git a/src/SmartHopper.Infrastructure/AIModels/AIModelPricing.cs b/src/SmartHopper.ProviderSdk/AIModels/AIModelPricing.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AIModels/AIModelPricing.cs rename to src/SmartHopper.ProviderSdk/AIModels/AIModelPricing.cs index 1be6047ad..7070a73df 100644 --- a/src/SmartHopper.Infrastructure/AIModels/AIModelPricing.cs +++ b/src/SmartHopper.ProviderSdk/AIModels/AIModelPricing.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.AIModels +namespace SmartHopper.ProviderSdk.AIModels { /// /// Pricing information for a model, in USD per unit as reported by the source diff --git a/src/SmartHopper.Infrastructure/AIModels/IAIProviderModels.cs b/src/SmartHopper.ProviderSdk/AIModels/IAIProviderModels.cs similarity index 97% rename from src/SmartHopper.Infrastructure/AIModels/IAIProviderModels.cs rename to src/SmartHopper.ProviderSdk/AIModels/IAIProviderModels.cs index 302594091..222ac697c 100644 --- a/src/SmartHopper.Infrastructure/AIModels/IAIProviderModels.cs +++ b/src/SmartHopper.ProviderSdk/AIModels/IAIProviderModels.cs @@ -18,8 +18,7 @@ using System.Collections.Generic; using System.Threading.Tasks; - -namespace SmartHopper.Infrastructure.AIModels +namespace SmartHopper.ProviderSdk.AIModels { /// /// Interface for AI provider model metadata retrieval. diff --git a/src/SmartHopper.Infrastructure/AIProviders/AIExtraDescriptor.cs b/src/SmartHopper.ProviderSdk/AIProviders/AIExtraDescriptor.cs similarity index 98% rename from src/SmartHopper.Infrastructure/AIProviders/AIExtraDescriptor.cs rename to src/SmartHopper.ProviderSdk/AIProviders/AIExtraDescriptor.cs index 73a9190de..d56e64b50 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/AIExtraDescriptor.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/AIExtraDescriptor.cs @@ -17,8 +17,7 @@ */ using System; - -namespace SmartHopper.Infrastructure.AIProviders +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Describes a single provider-specific extra parameter that can be surfaced diff --git a/src/SmartHopper.Infrastructure/AIProviders/AIProvider.cs b/src/SmartHopper.ProviderSdk/AIProviders/AIProvider.cs similarity index 95% rename from src/SmartHopper.Infrastructure/AIProviders/AIProvider.cs rename to src/SmartHopper.ProviderSdk/AIProviders/AIProvider.cs index 3244841eb..328389e6e 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/AIProvider.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/AIProvider.cs @@ -29,20 +29,19 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AITools; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Settings; -using SmartHopper.Infrastructure.Streaming; -using SmartHopper.Infrastructure.Utils; - -namespace SmartHopper.Infrastructure.AIProviders +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; +using SmartHopper.ProviderSdk.Streaming; +using SmartHopper.ProviderSdk.Utils; +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Base class for AI providers, encapsulating common logic. @@ -170,7 +169,7 @@ public virtual async Task InitializeProviderAsync() { // STEP 1: Register models FIRST to make them available for default value resolution // Prevent reloading capabilities if already initialized - if (!ModelManager.Instance.HasProviderCapabilities(this.Name)) + if (!AIModelCapabilityRegistry.Instance.HasProviderCapabilities(this.Name)) { Debug.WriteLine($"[{this.Name}] Registering model capabilities"); @@ -188,7 +187,7 @@ public virtual async Task InitializeProviderAsync() m.Provider = this.Name.ToLowerInvariant(); } - ModelManager.Instance.SetCapabilities(m); + AIModelCapabilityRegistry.Instance.SetCapabilities(m); Debug.WriteLine($"[{this.Name}] Registered model {m.Model} with capabilities {m.Capabilities.ToDetailedString()} and default {m.Default.ToDetailedString()}"); } } @@ -205,13 +204,11 @@ public virtual async Task InitializeProviderAsync() // Continue initialization even if model registration fails } - // STEP 2: Now load settings with models available for lazy default resolution - // Initialize the provider with its settings from SmartHopperSettings - var settingsDict = SmartHopperSettings.Instance.GetProviderSettings(this.Name); - if (settingsDict == null) - { - settingsDict = new Dictionary(); - } + // STEP 2: Now load settings with models available for lazy default resolution. + // Settings are retrieved through the host-injected per-provider settings store so + // the SDK does not depend on the host's SmartHopperSettings type. + var settingsStore = ProviderSdkHost.SettingsStoreFactory(this.Name); + var settingsDict = settingsStore?.GetAll() ?? new Dictionary(); // Load default values to a separate dictionary to prevent circular dependencies during retrieval this._defaultSettings = new Dictionary(); @@ -241,7 +238,9 @@ public virtual async Task InitializeProviderAsync() } // Apply all settings (stored + defaults) to the provider - this.RefreshCachedSettings(settingsDict); + this.RefreshCachedSettings(settingsDict is Dictionary concrete + ? concrete + : new Dictionary(settingsDict)); } /// @@ -466,7 +465,7 @@ public void RefreshCachedSettings(Dictionary settings) /// public virtual IEnumerable GetSettingDescriptors() { - var ui = ProviderManager.Instance.GetProviderSettings(this.Name); + var ui = ProviderSdkHost.ProviderRegistry.GetProviderSettings(this.Name); return ui != null ? ui.GetSettingDescriptors() : Enumerable.Empty(); } @@ -566,7 +565,7 @@ public string GetDefaultModel(AICapability requiredCapability = AICapability.Tex if (!string.IsNullOrWhiteSpace(modelFromSettings)) { - if (ModelManager.Instance.ValidateCapabilities(this.Name, modelFromSettings, requiredCapability)) + if (AIModelCapabilityRegistry.Instance.ValidateCapabilities(this.Name, modelFromSettings, requiredCapability)) { return modelFromSettings; } @@ -574,7 +573,7 @@ public string GetDefaultModel(AICapability requiredCapability = AICapability.Tex } // Else, try to get default model from ModelManager that matches the required capabilities - string modelFromProviderDefault = ModelManager.Instance.GetDefaultModel(this.Name, requiredCapability); + string modelFromProviderDefault = AIModelCapabilityRegistry.Instance.GetDefaultModel(this.Name, requiredCapability); if (!string.IsNullOrWhiteSpace(modelFromProviderDefault)) { @@ -591,7 +590,7 @@ public virtual string SelectModel(AICapability requiredCapability, string reques var preferredDefault = this.GetDefaultModel(requiredCapability, useSettings: true); // Delegate to centralized selection policy to avoid duplication - var selected = ModelManager.Instance.SelectBestModel(this.Name, requestedModel, requiredCapability, preferredDefault); + var selected = AIModelCapabilityRegistry.Instance.SelectBestModel(this.Name, requestedModel, requiredCapability, preferredDefault); return selected ?? string.Empty; } @@ -623,10 +622,9 @@ protected void SetSetting(string key, object value) this._injectedSettings[key] = value; - // Persist to the global settings with provider scoping - var settings = SmartHopper.Infrastructure.Settings.SmartHopperSettings.Instance; - settings.SetSetting(this.Name, key, value); - settings.Save(); + // Persist to the global settings via the host-injected per-provider store + var store = ProviderSdkHost.SettingsStoreFactory(this.Name); + store?.Set(key, value); Debug.WriteLine($"[{this.Name}] Setting '{key}' updated and persisted"); } @@ -644,7 +642,7 @@ protected void SetSetting(string key, object value) /// True if the setting has a non-empty value; otherwise, false. protected bool IsSettingConfigured(string settingName) { - var settings = SmartHopperSettings.Instance?.GetProviderSettings(this.Name); + var settings = ProviderSdkHost.SettingsStoreFactory?.Invoke(this.Name)?.GetAll(); if (settings == null || !settings.TryGetValue(settingName, out var value) || value == null) { return false; @@ -661,7 +659,7 @@ protected bool IsSettingConfigured(string settingName) /// True if the setting is a valid HTTP(S) URL; otherwise, false. protected bool IsUrlSettingConfigured(string settingName) { - var settings = SmartHopperSettings.Instance?.GetProviderSettings(this.Name); + var settings = ProviderSdkHost.SettingsStoreFactory?.Invoke(this.Name)?.GetAll(); if (settings == null || !settings.TryGetValue(settingName, out var value) || value == null) { return false; @@ -723,8 +721,8 @@ protected JArray GetFormattedTools(string toolFilter) { try { - AIToolManager.DiscoverTools(); - var tools = AIToolManager.GetTools(); + ProviderSdkHost.ToolRegistry.DiscoverTools(); + var tools = ProviderSdkHost.ToolRegistry.GetTools(); if (tools.Count == 0) { Debug.WriteLine("No tools available."); diff --git a/src/SmartHopper.Infrastructure/AIProviders/AIProviderModels.cs b/src/SmartHopper.ProviderSdk/AIProviders/AIProviderModels.cs similarity index 95% rename from src/SmartHopper.Infrastructure/AIProviders/AIProviderModels.cs rename to src/SmartHopper.ProviderSdk/AIProviders/AIProviderModels.cs index b3b3df5a3..b10b57a4d 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/AIProviderModels.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/AIProviderModels.cs @@ -19,9 +19,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AIModels; - -namespace SmartHopper.Infrastructure.AIProviders +using SmartHopper.ProviderSdk.AIModels; +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Base class for AI provider model metadata retrieval. diff --git a/src/SmartHopper.Infrastructure/AIProviders/AIProviderSettings.cs b/src/SmartHopper.ProviderSdk/AIProviders/AIProviderSettings.cs similarity index 87% rename from src/SmartHopper.Infrastructure/AIProviders/AIProviderSettings.cs rename to src/SmartHopper.ProviderSdk/AIProviders/AIProviderSettings.cs index f3eb63813..2579f567d 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/AIProviderSettings.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/AIProviderSettings.cs @@ -19,9 +19,9 @@ using System; using System.Collections.Generic; using System.Linq; -using SmartHopper.Infrastructure.Settings; - -namespace SmartHopper.Infrastructure.AIProviders +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Base class for provider settings, encapsulating common UI building and persistence logic. @@ -51,8 +51,9 @@ public virtual bool EnableStreaming { try { - // Try persisted value first - var value = SmartHopperSettings.Instance.GetSetting(this.Provider.Name, "EnableStreaming"); + // Try persisted value first via the host-injected settings store + var store = ProviderSdkHost.SettingsStoreFactory(this.Provider.Name); + var value = store?.Get("EnableStreaming"); if (value is bool b) { return b; diff --git a/src/SmartHopper.Infrastructure/AIProviders/AIProviderStreamingAdapter.cs b/src/SmartHopper.ProviderSdk/AIProviders/AIProviderStreamingAdapter.cs similarity index 99% rename from src/SmartHopper.Infrastructure/AIProviders/AIProviderStreamingAdapter.cs rename to src/SmartHopper.ProviderSdk/AIProviders/AIProviderStreamingAdapter.cs index d16bac34a..6e64352e3 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/AIProviderStreamingAdapter.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/AIProviderStreamingAdapter.cs @@ -25,9 +25,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Requests; -namespace SmartHopper.Infrastructure.AIProviders +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Base class providing shared helpers for provider-specific streaming adapters. diff --git a/src/SmartHopper.Infrastructure/AIProviders/IAIProvider.cs b/src/SmartHopper.ProviderSdk/AIProviders/IAIProvider.cs similarity index 95% rename from src/SmartHopper.Infrastructure/AIProviders/IAIProvider.cs rename to src/SmartHopper.ProviderSdk/AIProviders/IAIProvider.cs index d9868bd6c..415e74fed 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/IAIProvider.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/IAIProvider.cs @@ -21,14 +21,13 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.Settings; -using SmartHopper.Infrastructure.Streaming; - -namespace SmartHopper.Infrastructure.AIProviders +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.Settings; +using SmartHopper.ProviderSdk.Streaming; +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Defines the contract for AI provider integrations (e.g., MistralAI, OpenAI, Anthropic, DeepSeek). diff --git a/src/SmartHopper.Infrastructure/AIProviders/IAIProviderFactory.cs b/src/SmartHopper.ProviderSdk/AIProviders/IAIProviderFactory.cs similarity index 96% rename from src/SmartHopper.Infrastructure/AIProviders/IAIProviderFactory.cs rename to src/SmartHopper.ProviderSdk/AIProviders/IAIProviderFactory.cs index 7629b03a2..a0385263f 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/IAIProviderFactory.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/IAIProviderFactory.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.AIProviders +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Interface for AI provider factories. diff --git a/src/SmartHopper.Infrastructure/AIProviders/IAIProviderSettings.cs b/src/SmartHopper.ProviderSdk/AIProviders/IAIProviderSettings.cs similarity index 95% rename from src/SmartHopper.Infrastructure/AIProviders/IAIProviderSettings.cs rename to src/SmartHopper.ProviderSdk/AIProviders/IAIProviderSettings.cs index 096b4031f..0a99cbb7a 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/IAIProviderSettings.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/IAIProviderSettings.cs @@ -17,9 +17,8 @@ */ using System.Collections.Generic; -using SmartHopper.Infrastructure.Settings; - -namespace SmartHopper.Infrastructure.AIProviders +using SmartHopper.ProviderSdk.Settings; +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Interface for provider settings UI and validation. diff --git a/src/SmartHopper.Infrastructure/AIProviders/ProviderHttpStatusException.cs b/src/SmartHopper.ProviderSdk/AIProviders/ProviderHttpStatusException.cs similarity index 97% rename from src/SmartHopper.Infrastructure/AIProviders/ProviderHttpStatusException.cs rename to src/SmartHopper.ProviderSdk/AIProviders/ProviderHttpStatusException.cs index 40b21450a..21f51521c 100644 --- a/src/SmartHopper.Infrastructure/AIProviders/ProviderHttpStatusException.cs +++ b/src/SmartHopper.ProviderSdk/AIProviders/ProviderHttpStatusException.cs @@ -17,8 +17,7 @@ */ using System.Net.Http; - -namespace SmartHopper.Infrastructure.AIProviders +namespace SmartHopper.ProviderSdk.AIProviders { /// /// Exception thrown by streaming adapters when the provider returns a non-success HTTP status, diff --git a/src/SmartHopper.Infrastructure/Diagnostics/SHRuntimeMessage.cs b/src/SmartHopper.ProviderSdk/Diagnostics/SHRuntimeMessage.cs similarity index 99% rename from src/SmartHopper.Infrastructure/Diagnostics/SHRuntimeMessage.cs rename to src/SmartHopper.ProviderSdk/Diagnostics/SHRuntimeMessage.cs index 3e401b61a..0c2e5c3a3 100644 --- a/src/SmartHopper.Infrastructure/Diagnostics/SHRuntimeMessage.cs +++ b/src/SmartHopper.ProviderSdk/Diagnostics/SHRuntimeMessage.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.Diagnostics +namespace SmartHopper.ProviderSdk.Diagnostics { /// /// Severity levels for SmartHopper runtime messages. diff --git a/src/SmartHopper.ProviderSdk/Hosting/IContextProviderHost.cs b/src/SmartHopper.ProviderSdk/Hosting/IContextProviderHost.cs new file mode 100644 index 000000000..7e4f984f4 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IContextProviderHost.cs @@ -0,0 +1,49 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System.Collections.Generic; +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Surface that the SDK consumes to retrieve system-prompt context contributions + /// from the host's context manager (Grasshopper canvas state, user preferences, + /// document-scoped data, etc.). + /// + public interface IContextProviderHost + { + /// + /// Resolve the active context bag for the given provider filter expression. + /// Keys are provider/contributor identifiers and values are arbitrary text or + /// JSON fragments to be embedded in the system prompt. + /// + /// Optional space-separated list of contributor ids + /// to include. Pass null/empty to request the entire context bag. + IDictionary GetCurrentContext(string providerFilter = null); + } + + /// + /// No-op context provider that returns an empty dictionary. Used when SDK code runs + /// outside the SmartHopper host (e.g. unit tests). + /// + public sealed class NullContextProviderHost : IContextProviderHost + { + /// + public IDictionary GetCurrentContext(string providerFilter = null) + => new Dictionary(); + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/IPolicyPipelineHost.cs b/src/SmartHopper.ProviderSdk/Hosting/IPolicyPipelineHost.cs new file mode 100644 index 000000000..d5d51aee2 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IPolicyPipelineHost.cs @@ -0,0 +1,57 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System.Threading.Tasks; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Surface that the SDK consumes to invoke the host's request and response policy + /// pipeline. The SmartHopper host registers its PolicyPipeline with + /// at startup so DTO-side execution paths + /// can apply policies without referencing host orchestration types directly. + /// + public interface IPolicyPipelineHost + { + /// + /// Apply all configured request-side policies to + /// before it leaves the host. + /// + Task ApplyRequestPoliciesAsync(AIRequestCall request); + + /// + /// Apply all configured response-side policies to + /// after the provider returns. + /// + Task ApplyResponsePoliciesAsync(AIReturn response); + } + + /// + /// Default no-op policy pipeline used when no host has been registered. Lets SDK + /// code execute end-to-end in tests and stand-alone tools without any orchestration. + /// + public sealed class NullPolicyPipelineHost : IPolicyPipelineHost + { + /// + public Task ApplyRequestPoliciesAsync(AIRequestCall request) => Task.CompletedTask; + + /// + public Task ApplyResponsePoliciesAsync(AIReturn response) => Task.CompletedTask; + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/IProviderDiagnostics.cs b/src/SmartHopper.ProviderSdk/Hosting/IProviderDiagnostics.cs new file mode 100644 index 000000000..fe3220722 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IProviderDiagnostics.cs @@ -0,0 +1,45 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using SmartHopper.ProviderSdk.Diagnostics; +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Surface that providers use to emit user-visible runtime messages and metrics + /// without depending on host-side runtime-message UI. + /// + public interface IProviderDiagnostics + { + /// + /// Surface a runtime message to the user. The host decides where to render it + /// (component balloon, WebChat banner, status bar, etc.). + /// + void Report(string providerName, SHRuntimeMessage message); + } + + /// + /// No-op diagnostics sink used when SDK code runs without a host attached. + /// + public sealed class NullProviderDiagnostics : IProviderDiagnostics + { + /// + public void Report(string providerName, SHRuntimeMessage message) + { + } + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/IProviderHttpClientFactory.cs b/src/SmartHopper.ProviderSdk/Hosting/IProviderHttpClientFactory.cs new file mode 100644 index 000000000..4d552e774 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IProviderHttpClientFactory.cs @@ -0,0 +1,56 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Net.Http; +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Surface that providers use to obtain configured + /// instances. The host can centralize timeouts, proxies, and retry behavior here so + /// individual providers do not need to repeat the boilerplate. + /// + public interface IProviderHttpClientFactory + { + /// + /// Create or fetch an scoped to + /// with the given total request . + /// + HttpClient CreateClient(string providerName, TimeSpan timeout); + } + + /// + /// Default implementation that returns a fresh per call + /// with the requested timeout. The SmartHopper host swaps in a smarter + /// pooled/factory-based implementation at startup. + /// + public sealed class DefaultProviderHttpClientFactory : IProviderHttpClientFactory + { + /// + public HttpClient CreateClient(string providerName, TimeSpan timeout) + { + var client = new HttpClient(); + if (timeout > TimeSpan.Zero) + { + client.Timeout = timeout; + } + + return client; + } + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/IProviderLogger.cs b/src/SmartHopper.ProviderSdk/Hosting/IProviderLogger.cs new file mode 100644 index 000000000..f72615e60 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IProviderLogger.cs @@ -0,0 +1,83 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Diagnostics; +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Severity classification for log messages emitted from SDK code. + /// + public enum ProviderLogLevel + { + /// Verbose tracing useful only when debugging the provider. + Trace, + + /// Routine informational messages. + Info, + + /// Recoverable anomalies the user should know about. + Warning, + + /// Unrecoverable failures within a single provider call. + Error, + } + + /// + /// Surface that providers use for log output without depending on Rhino's + /// RhinoApp.WriteLine directly. The host registers an implementation that + /// routes to its preferred logging sink. + /// + public interface IProviderLogger + { + /// + /// Log a message scoped to a provider id. + /// + void Log(ProviderLogLevel level, string providerName, string message); + + /// + /// Log an exception scoped to a provider id. + /// + void LogException(string providerName, Exception exception, string context = null); + } + + /// + /// Default logger that writes to . Suitable for tests and for + /// stand-alone tooling that does not have a richer sink available. + /// + public sealed class DebugProviderLogger : IProviderLogger + { + /// + public void Log(ProviderLogLevel level, string providerName, string message) + { + Debug.WriteLine($"[{level}][{providerName}] {message}"); + } + + /// + public void LogException(string providerName, Exception exception, string context = null) + { + if (exception == null) + { + return; + } + + var prefix = string.IsNullOrEmpty(context) ? string.Empty : context + ": "; + Debug.WriteLine($"[Error][{providerName}] {prefix}{exception.GetType().Name}: {exception.Message}"); + } + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/IProviderRegistryHost.cs b/src/SmartHopper.ProviderSdk/Hosting/IProviderRegistryHost.cs new file mode 100644 index 000000000..202d037ca --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IProviderRegistryHost.cs @@ -0,0 +1,53 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using SmartHopper.ProviderSdk.AIProviders; +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Surface that the SDK consumes to look up provider instances and their settings + /// objects without depending on the host's ProviderManager. The SmartHopper + /// host registers a real implementation with . + /// + public interface IProviderRegistryHost + { + /// + /// Resolve a registered provider by name, or null if the host has not loaded one. + /// + IAIProvider GetProvider(string providerName); + + /// + /// Resolve the live bound to the named provider, or + /// null when the provider is not registered. + /// + IAIProviderSettings GetProviderSettings(string providerName); + } + + /// + /// Null implementation that always returns null. Used when SDK code runs + /// outside a host (for example, unit tests covering pure DTO behavior). + /// + public sealed class NullProviderRegistryHost : IProviderRegistryHost + { + /// + public IAIProvider GetProvider(string providerName) => null; + + /// + public IAIProviderSettings GetProviderSettings(string providerName) => null; + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/IProviderSettingsStore.cs b/src/SmartHopper.ProviderSdk/Hosting/IProviderSettingsStore.cs new file mode 100644 index 000000000..9177c23ab --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IProviderSettingsStore.cs @@ -0,0 +1,118 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System.Collections.Generic; +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Per-provider settings store. The host scopes each instance to a single provider + /// id so a community provider cannot read or write secrets belonging to another + /// provider through this surface. + /// + public interface IProviderSettingsStore + { + /// + /// Provider id this store is scoped to. + /// + string ProviderName { get; } + + /// + /// Retrieve the full settings dictionary for this provider, or an empty dictionary + /// when none have been persisted yet. + /// + IDictionary GetAll(); + + /// + /// Persist the full settings dictionary for this provider. + /// + void SetAll(IDictionary settings); + + /// + /// Read a single setting by key. Returns when + /// the key is missing or the stored value is not assignable to . + /// + T Get(string key, T defaultValue = default); + + /// + /// Write a single setting value by key. + /// + void Set(string key, T value); + } + + /// + /// In-memory used when SDK code runs outside the + /// SmartHopper host (unit tests, sample provider self-checks, etc.). Per provider id. + /// + public sealed class InMemoryProviderSettingsStore : IProviderSettingsStore + { + private readonly Dictionary _storage = new Dictionary(); + + /// Initializes a new in-memory store scoped to . + public InMemoryProviderSettingsStore(string providerName) + { + this.ProviderName = providerName ?? string.Empty; + } + + /// + public string ProviderName { get; } + + /// + public IDictionary GetAll() => new Dictionary(this._storage); + + /// + public void SetAll(IDictionary settings) + { + this._storage.Clear(); + if (settings == null) + { + return; + } + + foreach (var kvp in settings) + { + this._storage[kvp.Key] = kvp.Value; + } + } + + /// + public T Get(string key, T defaultValue = default) + { + if (!this._storage.TryGetValue(key, out var value) || value == null) + { + return defaultValue; + } + + if (value is T typed) + { + return typed; + } + + try + { + return (T)System.Convert.ChangeType(value, typeof(T)); + } + catch + { + return defaultValue; + } + } + + /// + public void Set(string key, T value) => this._storage[key] = value; + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/IProviderTrustHost.cs b/src/SmartHopper.ProviderSdk/Hosting/IProviderTrustHost.cs new file mode 100644 index 000000000..edf73ff46 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IProviderTrustHost.cs @@ -0,0 +1,87 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Surface that the SDK consumes to reason about per-provider trust and integrity + /// state. Implemented by the SmartHopper host and registered with + /// at startup. + /// + public interface IProviderTrustHost + { + /// + /// Effective provider integrity check mode after applying environment overrides + /// (for example, forcing Soft in DEBUG builds). + /// + ProviderIntegrityCheckMode EffectiveIntegrityCheckMode { get; } + + /// + /// Returns true when the provider's local hash does not match the published manifest. + /// + bool IsProviderMismatched(string providerName); + + /// + /// Returns true when integrity verification could not be performed (manifest + /// unreachable, network issue, etc.). + /// + bool IsProviderUnavailable(string providerName); + + /// + /// Returns true when the provider has no entry in the official manifest + /// (custom/third-party provider). + /// + bool IsProviderUnknown(string providerName); + + /// + /// Returns true when the provider was classified as community / non-official. + /// + bool IsProviderCommunity(string providerName); + + /// + /// Returns true when the provider was loaded without any strong-name signature. + /// + bool IsProviderUnsigned(string providerName); + } + + /// + /// Default no-op implementation used when no host has registered a real trust host. + /// Treats every provider as official and reports Soft integrity mode so SDK + /// validation surfaces warnings rather than errors when running outside the host. + /// + public sealed class NullProviderTrustHost : IProviderTrustHost + { + /// + public ProviderIntegrityCheckMode EffectiveIntegrityCheckMode => ProviderIntegrityCheckMode.Soft; + + /// + public bool IsProviderMismatched(string providerName) => false; + + /// + public bool IsProviderUnavailable(string providerName) => false; + + /// + public bool IsProviderUnknown(string providerName) => false; + + /// + public bool IsProviderCommunity(string providerName) => false; + + /// + public bool IsProviderUnsigned(string providerName) => false; + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/IToolRegistryHost.cs b/src/SmartHopper.ProviderSdk/Hosting/IToolRegistryHost.cs new file mode 100644 index 000000000..85dfad231 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/IToolRegistryHost.cs @@ -0,0 +1,86 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System.Collections.Generic; +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Minimal projection of a tool exposed by the host's tool manager, scoped to the + /// fields a provider needs when formatting tools for an LLM request. + /// + public sealed class ProviderToolDefinition + { + /// Tool identifier (matches host-side AITool.Name). + public string Name { get; set; } + + /// Human-readable description supplied to the model. + public string Description { get; set; } + + /// Rich human-readable description supplied to the model. + public string RichDescription => string.IsNullOrEmpty(this.Description) ? string.Empty : this.Description; + + /// Whether the tool is enabled and available for use. + public bool Enabled { get; set; } = true; + + /// Tool category (e.g. knowledge, script); free-form. + public string Category { get; set; } + + /// JSON schema fragment describing the tool's argument shape. + public string ParametersSchema { get; set; } + + /// Optional model-side hint. Free-form (e.g. required). + public string RequiredUse { get; set; } + } + + /// + /// Surface that the SDK consumes to enumerate tools registered with the host's tool + /// manager. The host registers an implementation with + /// ; SDK code calls + /// to trigger lazy discovery and + /// to read the current catalog. + /// + public interface IToolRegistryHost + { + /// + /// Trigger lazy discovery of tools. Idempotent; may be called more than once. + /// + void DiscoverTools(); + + /// + /// Return the current snapshot of registered tools keyed by tool name. + /// + IReadOnlyDictionary GetTools(); + } + + /// + /// Empty tool registry used when SDK code runs outside the SmartHopper host. + /// + public sealed class NullToolRegistryHost : IToolRegistryHost + { + private static readonly Dictionary Empty + = new Dictionary(); + + /// + public void DiscoverTools() + { + } + + /// + public IReadOnlyDictionary GetTools() => Empty; + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/ProviderIntegrityCheckMode.cs b/src/SmartHopper.ProviderSdk/Hosting/ProviderIntegrityCheckMode.cs new file mode 100644 index 000000000..7dc6ce940 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/ProviderIntegrityCheckMode.cs @@ -0,0 +1,46 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Provider integrity check mode defining how verification failures are handled by + /// the host trust pipeline. Lives in the SDK so that DTOs in SDK code paths can + /// reason about it without depending on the SmartHopper host. + /// + public enum ProviderIntegrityCheckMode + { + /// + /// Strict mode: blocks providers on hash mismatch, unavailable hash repository, + /// and unknown providers (not present in the official manifest). + /// + Strict, + + /// + /// Hard mode: blocks providers on hash mismatch and unknown providers, but + /// allows loading when the hash repository is unreachable. + /// + Hard, + + /// + /// Soft mode: surfaces warnings rather than blocking. Best suited for + /// development and third-party providers. + /// + Soft, + } +} diff --git a/src/SmartHopper.ProviderSdk/Hosting/ProviderSdkHost.cs b/src/SmartHopper.ProviderSdk/Hosting/ProviderSdkHost.cs new file mode 100644 index 000000000..f28f81832 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Hosting/ProviderSdkHost.cs @@ -0,0 +1,90 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +namespace SmartHopper.ProviderSdk.Hosting +{ + /// + /// Static composition root the SmartHopper host uses at startup to inject + /// host-side services into SDK code paths. Default null/no-op implementations are + /// provided so SDK code remains executable in unit tests and stand-alone tools. + /// + /// + /// + /// The host registers concrete implementations exactly once during plugin + /// initialization; SDK code reads the configured property each time it needs a + /// service so registration order does not matter. + /// + /// + /// This is a runtime extension point only — provider authors must not depend on + /// specific host implementations or rely on host registrations happening before + /// provider activation. + /// + /// + public static class ProviderSdkHost + { + /// + /// Per-provider trust/integrity status surface used by request validation. + /// + public static IProviderTrustHost ProviderTrust { get; set; } = new NullProviderTrustHost(); + + /// + /// Provider lookup surface used by DTOs and the AIProvider base class. + /// + public static IProviderRegistryHost ProviderRegistry { get; set; } = new NullProviderRegistryHost(); + + /// + /// Host policy pipeline invoked by SDK execution paths around request/response. + /// + public static IPolicyPipelineHost PolicyPipeline { get; set; } = new NullPolicyPipelineHost(); + + /// + /// Context bag provider used by SDK system-prompt assembly. + /// + public static IContextProviderHost ContextProvider { get; set; } = new NullContextProviderHost(); + + /// + /// Tool registry surface used when an SDK request formats tools for an LLM. + /// + public static IToolRegistryHost ToolRegistry { get; set; } = new NullToolRegistryHost(); + + /// + /// Diagnostics sink for user-visible runtime messages emitted from SDK code. + /// + public static IProviderDiagnostics Diagnostics { get; set; } = new NullProviderDiagnostics(); + + /// + /// Logger used by the AIProvider base class and SDK utilities. + /// + public static IProviderLogger Logger { get; set; } = new DebugProviderLogger(); + + /// + /// HTTP client factory used by the AIProvider base class. The host installs a + /// pooled factory at startup; tests can swap a mock factory here. + /// + public static IProviderHttpClientFactory HttpClientFactory { get; set; } = new DefaultProviderHttpClientFactory(); + + /// + /// Factory used by the AIProvider base class to obtain a per-provider settings + /// store scoped to a single provider id. The default returns an in-memory store + /// so tests can exercise SDK paths without a host attached; the SmartHopper host + /// replaces this with a real, persistence-backed factory at startup. + /// + public static System.Func SettingsStoreFactory { get; set; } + = providerName => new InMemoryProviderSettingsStore(providerName); + } +} diff --git a/src/SmartHopper.ProviderSdk/LICENSE b/src/SmartHopper.ProviderSdk/LICENSE new file mode 100644 index 000000000..535881452 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-2026 Marc Roca Musach + SmartHopper Community + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/SmartHopper.ProviderSdk/Metadata/AssemblyInfo.cs b/src/SmartHopper.ProviderSdk/Metadata/AssemblyInfo.cs new file mode 100644 index 000000000..c65cfadce --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Metadata/AssemblyInfo.cs @@ -0,0 +1,28 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using SmartHopper.ProviderSdk.Metadata; +// Declare the SemVer of the contract surface exposed by SmartHopper.ProviderSdk. +// Providers declare BuiltAgainstSdk("1.0.0") and MinHostSdk("1.0.0") against this value. +// MAJOR is bumped on breaking provider-contract changes. +[assembly: SmartHopperProviderSdkVersion("1.0.0")] + +namespace SmartHopper.ProviderSdk.Metadata +{ +} + diff --git a/src/SmartHopper.ProviderSdk/Metadata/SdkCompatibility.cs b/src/SmartHopper.ProviderSdk/Metadata/SdkCompatibility.cs new file mode 100644 index 000000000..6f8d0ec4a --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Metadata/SdkCompatibility.cs @@ -0,0 +1,142 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Linq; +using System.Reflection; +namespace SmartHopper.ProviderSdk.Metadata +{ + /// + /// Helpers for reading provider SDK version metadata and checking SemVer + /// compatibility between provider assemblies and the host's SDK copy. + /// + public static class SdkCompatibility + { + /// + /// The version of declared by + /// the currently loaded SmartHopper.ProviderSdk assembly. + /// + public static string HostSdkVersion { get; } = + typeof(SdkCompatibility).Assembly + .GetCustomAttributes() + .FirstOrDefault() + ?.Version + ?? "0.0.0"; + + /// + /// Outcome of a call. + /// + public enum CompatibilityResult + { + /// The provider is compatible with the host SDK. + Compatible, + + /// The provider's BuiltAgainstSdk MAJOR does not match the host SDK MAJOR. + MajorMismatch, + + /// The host SDK version is lower than the provider's declared MinHostSdk. + HostTooOld, + + /// The provider does not declare its BuiltAgainstSdk at all. + MissingMetadata, + } + + /// + /// Check whether the provider assembly is compatible with the host SDK based on + /// and . + /// + /// The loaded provider assembly to inspect. + /// A user-facing diagnostic on failure; empty on success. + public static CompatibilityResult Check(Assembly providerAssembly, out string diagnostic) + { + if (providerAssembly is null) + { + throw new ArgumentNullException(nameof(providerAssembly)); + } + + var builtAgainst = providerAssembly + .GetCustomAttributes() + .FirstOrDefault() + ?.Version; + var minHost = providerAssembly + .GetCustomAttributes() + .FirstOrDefault() + ?.Version; + + if (string.IsNullOrWhiteSpace(builtAgainst)) + { + diagnostic = $"Provider '{providerAssembly.GetName().Name}' does not declare [BuiltAgainstSdk]; SDK compatibility cannot be verified."; + return CompatibilityResult.MissingMetadata; + } + + if (GetMajor(builtAgainst) != GetMajor(HostSdkVersion)) + { + diagnostic = $"Provider '{providerAssembly.GetName().Name}' was built against SDK {builtAgainst}; host SDK is {HostSdkVersion}. Major versions must match."; + return CompatibilityResult.MajorMismatch; + } + + if (!string.IsNullOrWhiteSpace(minHost) && Compare(HostSdkVersion, minHost!) < 0) + { + diagnostic = $"Provider '{providerAssembly.GetName().Name}' requires host SDK >= {minHost}, but host SDK is {HostSdkVersion}."; + return CompatibilityResult.HostTooOld; + } + + diagnostic = string.Empty; + return CompatibilityResult.Compatible; + } + + private static int GetMajor(string semVer) + { + if (string.IsNullOrWhiteSpace(semVer)) + { + return 0; + } + + var idx = semVer.IndexOf('.'); + var head = idx > 0 ? semVer.Substring(0, idx) : semVer; + return int.TryParse(head, out var major) ? major : 0; + } + + private static int Compare(string a, string b) + { + int[] pa = Parse(a); + int[] pb = Parse(b); + for (int i = 0; i < 3; i++) + { + if (pa[i] != pb[i]) + { + return pa[i].CompareTo(pb[i]); + } + } + + return 0; + } + + private static int[] Parse(string v) + { + var parts = (v ?? string.Empty).Split('.', '-', '+'); + var result = new int[3]; + for (int i = 0; i < 3 && i < parts.Length; i++) + { + int.TryParse(parts[i], out result[i]); + } + + return result; + } + } +} diff --git a/src/SmartHopper.ProviderSdk/Metadata/SmartHopperProviderSdkVersionAttribute.cs b/src/SmartHopper.ProviderSdk/Metadata/SmartHopperProviderSdkVersionAttribute.cs new file mode 100644 index 000000000..e8ec66675 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/Metadata/SmartHopperProviderSdkVersionAttribute.cs @@ -0,0 +1,119 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +namespace SmartHopper.ProviderSdk.Metadata +{ + /// + /// Identifies the SmartHopper Provider SDK version of an assembly. + /// + /// + /// Applied automatically to SmartHopper.ProviderSdk itself (the host queries + /// this attribute to determine which SDK version it ships with), and intended to be + /// applied by community provider authors via + /// and to declare compatibility ranges. + /// + /// + /// + /// SemVer is used: MAJOR bumps indicate a breaking provider contract change. + /// Providers are loaded only when BuiltAgainstSdk.MAJOR == HostSdk.MAJOR and + /// HostSdk >= provider.MinHostSdk. Mismatches classify the provider as + /// Invalid and block the load with a clear error. + /// + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] + public sealed class SmartHopperProviderSdkVersionAttribute : Attribute + { + /// + /// Gets the SemVer version string identifying the SDK contract. + /// + public string Version { get; } + + /// + /// Initializes a new instance of the class. + /// + /// A SemVer string such as "1.0.0". + public SmartHopperProviderSdkVersionAttribute(string version) + { + this.Version = version ?? throw new ArgumentNullException(nameof(version)); + } + } + + /// + /// Applied by provider authors to declare which SDK version their assembly was + /// compiled against. Combined with , it lets the + /// host reject providers built against an incompatible SDK MAJOR before any code + /// from the provider runs. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] + public sealed class BuiltAgainstSdkAttribute : Attribute + { + /// Gets the SDK version this provider was built against. + public string Version { get; } + + /// + /// Initializes a new instance of the class. + /// + /// A SemVer string such as "1.0.0". + public BuiltAgainstSdkAttribute(string version) + { + this.Version = version ?? throw new ArgumentNullException(nameof(version)); + } + } + + /// + /// Applied by provider authors to declare the minimum host SDK version they require. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] + public sealed class MinHostSdkAttribute : Attribute + { + /// Gets the minimum host SDK version this provider supports. + public string Version { get; } + + /// + /// Initializes a new instance of the class. + /// + /// A SemVer string such as "1.0.0". + public MinHostSdkAttribute(string version) + { + this.Version = version ?? throw new ArgumentNullException(nameof(version)); + } + } + + /// + /// Optional metadata applied by provider authors to declare a stable provider + /// identifier independent of the assembly name. Used during conflict resolution and + /// classification but is NOT a security boundary — names are easy to spoof; trust + /// decisions are still based on cryptographic checks. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] + public sealed class SmartHopperProviderIdAttribute : Attribute + { + /// Gets the provider identifier declared by the assembly. + public string ProviderId { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The provider identifier (e.g. "openai"). + public SmartHopperProviderIdAttribute(string providerId) + { + this.ProviderId = providerId ?? throw new ArgumentNullException(nameof(providerId)); + } + } +} diff --git a/src/SmartHopper.ProviderSdk/README.md b/src/SmartHopper.ProviderSdk/README.md new file mode 100644 index 000000000..b21630001 --- /dev/null +++ b/src/SmartHopper.ProviderSdk/README.md @@ -0,0 +1,64 @@ +# SmartHopper.ProviderSdk + +Stable, MIT-licensed SDK for building third-party AI providers for [SmartHopper](https://github.com/architects-toolkit/SmartHopper). + +Community providers built against this SDK can compile on a clean machine without +the SmartHopper repository, produce a `SmartHopper.Providers..dll`, and drop +it into the SmartHopper provider folder. + +## License + +This SDK is licensed under the **MIT License** (see [LICENSE](LICENSE)). + +This is intentional: the SmartHopper host is licensed under **LGPLv3**, but the +SDK that providers link against is permissive so that closed-source community +providers can link to it without inheriting LGPL obligations on their own +assemblies. + +## What this package contains + +- Provider contracts: `IAIProvider`, `IAIProviderFactory`, `IAIProviderSettings`, `IAIProviderModels`. +- Provider base classes: `AIProvider`, `AIProvider`, `AIProviderSettings`, `AIProviderModels`, `AIProviderStreamingAdapter`. +- Request/response DTOs: `AICall.Core.Interactions`, `AICall.Core.Requests`, `AICall.Core.Returns`, `AICall.Core.Base`, `AICall.Metrics`. +- Model capability types: `AIModels.AICapability`, `AIModels.AIModelCapabilities`, `AIModels.AIModelPricing`, `AIExtraDescriptor`. +- Settings descriptors: `SettingDescriptor`, secret flags, validation result types. +- Streaming: `IStreamingAdapter` and provider-facing delta/result types. +- Host-injected abstractions a provider receives: `IProviderSettingsStore`, + `IProviderLogger`, `IProviderHttpClientFactory`, `IProviderDiagnostics`. +- Compatibility metadata: `SmartHopperProviderSdkVersionAttribute`, + `SmartHopperProviderIdAttribute`, `BuiltAgainstSdkAttribute`. + +## What this package does NOT contain + +The following remain in the SmartHopper host and are NOT exposed through the SDK: + +- `ProviderManager`, hash/signature verification, dialogs, settings persistence. +- Policy pipeline, conversation sessions, AICall validation pipeline, execution, + batch orchestration. +- Tool manager, tool registry, `IAIToolProvider` (tool DTOs in interactions are + included so providers can encode/decode tool calls in responses, but tool + registration is host-only). +- Rhino/Eto UI, WebChat, badges, runtime message UI. + +## Target frameworks + +- `net7.0-windows` +- `net7.0` + +## Getting started + +```bash +dotnet new classlib -n SmartHopper.Providers.MyProvider +dotnet add SmartHopper.Providers.MyProvider package SmartHopper.ProviderSdk +``` + +Then implement an `IAIProviderFactory` that returns your `AIProvider` +subclass. Build with target frameworks `net7.0-windows;net7.0` and drop the +resulting `SmartHopper.Providers.MyProvider.dll` into: + +- **App-local**: the same folder as `SmartHopper.Infrastructure.dll`, or +- **User-local**: `%AppData%\SmartHopper\Providers\` (Windows) / + `~/.config/SmartHopper/Providers/` (macOS). + +See [docs/Providers/ProviderSdk.md](https://github.com/architects-toolkit/SmartHopper/blob/main/docs/Providers/ProviderSdk.md) +in the SmartHopper repository for the full guide. diff --git a/src/SmartHopper.Infrastructure/Settings/SettingDescriptor.cs b/src/SmartHopper.ProviderSdk/Settings/SettingDescriptor.cs similarity index 99% rename from src/SmartHopper.Infrastructure/Settings/SettingDescriptor.cs rename to src/SmartHopper.ProviderSdk/Settings/SettingDescriptor.cs index 8c2ae668b..75e12dab8 100644 --- a/src/SmartHopper.Infrastructure/Settings/SettingDescriptor.cs +++ b/src/SmartHopper.ProviderSdk/Settings/SettingDescriptor.cs @@ -18,8 +18,7 @@ using System; using System.Collections.Generic; - -namespace SmartHopper.Infrastructure.Settings +namespace SmartHopper.ProviderSdk.Settings { /// /// Wrapper for lazy evaluation of default values. diff --git a/src/SmartHopper.Infrastructure/Settings/SettingDescriptorControl.cs b/src/SmartHopper.ProviderSdk/Settings/SettingDescriptorControl.cs similarity index 96% rename from src/SmartHopper.Infrastructure/Settings/SettingDescriptorControl.cs rename to src/SmartHopper.ProviderSdk/Settings/SettingDescriptorControl.cs index 4255bbe97..7fe990acf 100644 --- a/src/SmartHopper.Infrastructure/Settings/SettingDescriptorControl.cs +++ b/src/SmartHopper.ProviderSdk/Settings/SettingDescriptorControl.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.Settings +namespace SmartHopper.ProviderSdk.Settings { /// /// Base class for UI control parameters for a setting descriptor. diff --git a/src/SmartHopper.ProviderSdk/SmartHopper.ProviderSdk.csproj b/src/SmartHopper.ProviderSdk/SmartHopper.ProviderSdk.csproj new file mode 100644 index 000000000..90dbfadda --- /dev/null +++ b/src/SmartHopper.ProviderSdk/SmartHopper.ProviderSdk.csproj @@ -0,0 +1,68 @@ + + + + + net7.0-windows;net7.0 + NU1701;NETSDK1086;SA1124;SA1200 + true + true + true + + + + SmartHopper Provider SDK + Stable, MIT-licensed SDK for building SmartHopper AI provider plugins (e.g. SmartHopper.Providers.OpenAI). Provides the contracts, base classes, request/response DTOs, model capability types, and host-injected abstractions a provider needs to talk to its API and describe its models. + SmartHopper.ProviderSdk + MIT + smarthopper;grasshopper;rhino;ai;provider;sdk + https://github.com/architects-toolkit/SmartHopper + git + true + true + snupkg + + + + $(SolutionDir)bin/$(SolutionVersion)/$(Configuration) + $(MSBuildThisFileDirectory)../../packages + + + + + + + + true + $(DefineConstants);WINDOWS + + + + + + + + + + + + + + + + diff --git a/src/SmartHopper.Infrastructure/Streaming/IStreamingAdapter.cs b/src/SmartHopper.ProviderSdk/Streaming/IStreamingAdapter.cs similarity index 94% rename from src/SmartHopper.Infrastructure/Streaming/IStreamingAdapter.cs rename to src/SmartHopper.ProviderSdk/Streaming/IStreamingAdapter.cs index 83646e559..2407ea339 100644 --- a/src/SmartHopper.Infrastructure/Streaming/IStreamingAdapter.cs +++ b/src/SmartHopper.ProviderSdk/Streaming/IStreamingAdapter.cs @@ -18,12 +18,10 @@ using System.Collections.Generic; using System.Threading; - -namespace SmartHopper.Infrastructure.Streaming +namespace SmartHopper.ProviderSdk.Streaming { - using SmartHopper.Infrastructure.AICall.Core.Requests; - using SmartHopper.Infrastructure.AICall.Core.Returns; - + using SmartHopper.ProviderSdk.AICall.Core.Requests; + using SmartHopper.ProviderSdk.AICall.Core.Returns; /// /// Defines the contract for provider-specific streaming adapters that yield incremental AIReturn deltas. /// diff --git a/src/SmartHopper.Infrastructure/Utilities/JsonFormatHelper.cs b/src/SmartHopper.ProviderSdk/Utilities/JsonFormatHelper.cs similarity index 99% rename from src/SmartHopper.Infrastructure/Utilities/JsonFormatHelper.cs rename to src/SmartHopper.ProviderSdk/Utilities/JsonFormatHelper.cs index d8442cd93..1ee2effb9 100644 --- a/src/SmartHopper.Infrastructure/Utilities/JsonFormatHelper.cs +++ b/src/SmartHopper.ProviderSdk/Utilities/JsonFormatHelper.cs @@ -22,8 +22,7 @@ using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; - -namespace SmartHopper.Infrastructure.Utilities +namespace SmartHopper.ProviderSdk.Utilities { /// /// Centralized utility for consistent JSON formatting across all JSON components and tools. diff --git a/src/SmartHopper.Infrastructure/Utilities/JsonPathHelper.cs b/src/SmartHopper.ProviderSdk/Utilities/JsonPathHelper.cs similarity index 98% rename from src/SmartHopper.Infrastructure/Utilities/JsonPathHelper.cs rename to src/SmartHopper.ProviderSdk/Utilities/JsonPathHelper.cs index 14172ab00..f875c4bcb 100644 --- a/src/SmartHopper.Infrastructure/Utilities/JsonPathHelper.cs +++ b/src/SmartHopper.ProviderSdk/Utilities/JsonPathHelper.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -namespace SmartHopper.Infrastructure.Utilities +namespace SmartHopper.ProviderSdk.Utilities { /// /// Helper class for JSON path operations and error message formatting. diff --git a/src/SmartHopper.Infrastructure/Utils/Filter.cs b/src/SmartHopper.ProviderSdk/Utils/Filter.cs similarity index 98% rename from src/SmartHopper.Infrastructure/Utils/Filter.cs rename to src/SmartHopper.ProviderSdk/Utils/Filter.cs index 35c131bdc..d893d3962 100644 --- a/src/SmartHopper.Infrastructure/Utils/Filter.cs +++ b/src/SmartHopper.ProviderSdk/Utils/Filter.cs @@ -20,8 +20,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; - -namespace SmartHopper.Infrastructure.Utils +namespace SmartHopper.ProviderSdk.Utils { /// /// Encapsulates include/exclude rules parsed from a filter string diff --git a/src/SmartHopper.Infrastructure/Utils/HttpHeadersHelper.cs b/src/SmartHopper.ProviderSdk/Utils/HttpHeadersHelper.cs similarity index 98% rename from src/SmartHopper.Infrastructure/Utils/HttpHeadersHelper.cs rename to src/SmartHopper.ProviderSdk/Utils/HttpHeadersHelper.cs index 77feace9f..a2d7e5c9f 100644 --- a/src/SmartHopper.Infrastructure/Utils/HttpHeadersHelper.cs +++ b/src/SmartHopper.ProviderSdk/Utils/HttpHeadersHelper.cs @@ -19,8 +19,7 @@ using System; using System.Collections.Generic; using System.Net.Http; - -namespace SmartHopper.Infrastructure.Utils +namespace SmartHopper.ProviderSdk.Utils { /// /// Shared helper for applying request-scoped HTTP headers consistently across diff --git a/src/SmartHopper.Providers.Anthropic/AnthropicProvider.cs b/src/SmartHopper.Providers.Anthropic/AnthropicProvider.cs index ec895b40f..1029f0d78 100644 --- a/src/SmartHopper.Providers.Anthropic/AnthropicProvider.cs +++ b/src/SmartHopper.Providers.Anthropic/AnthropicProvider.cs @@ -29,20 +29,20 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.JsonSchemas; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Streaming; -using SmartHopper.Infrastructure.Utilities; -using SmartHopper.Infrastructure.Utils; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Streaming; +using SmartHopper.ProviderSdk.Utilities; +using SmartHopper.ProviderSdk.Utils; namespace SmartHopper.Providers.Anthropic { @@ -1312,7 +1312,7 @@ private static bool SupportsStructuredOutputs(string model) return false; } - var caps = ModelManager.Instance.GetCapabilities("Anthropic", model); + var caps = AIModelCapabilityRegistry.Instance.GetCapabilities("Anthropic", model); return caps?.HasCapability(AICapability.Text2Json) == true; } diff --git a/src/SmartHopper.Providers.Anthropic/AnthropicProviderFactory.cs b/src/SmartHopper.Providers.Anthropic/AnthropicProviderFactory.cs index 42b099a9d..8300b894b 100644 --- a/src/SmartHopper.Providers.Anthropic/AnthropicProviderFactory.cs +++ b/src/SmartHopper.Providers.Anthropic/AnthropicProviderFactory.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.Anthropic { diff --git a/src/SmartHopper.Providers.Anthropic/AnthropicProviderModels.cs b/src/SmartHopper.Providers.Anthropic/AnthropicProviderModels.cs index 0c3df1353..a1963b225 100644 --- a/src/SmartHopper.Providers.Anthropic/AnthropicProviderModels.cs +++ b/src/SmartHopper.Providers.Anthropic/AnthropicProviderModels.cs @@ -21,10 +21,10 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.Anthropic { @@ -56,6 +56,7 @@ public override Task> RetrieveModels() Model = "claude-sonnet-5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, Rank = 10000, ContextLimit = 1000000, @@ -68,6 +69,15 @@ public override Task> RetrieveModels() InputCacheWrite = 0.0000025m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000005m, + InputCacheRead = 0.0000001m, + InputCacheWrite = 0.00000125m, + WebSearch = 0.01m, + }, + Aliases = new List { "claude-sonnet-5:batch" }, }, new AIModelCapabilities @@ -76,6 +86,7 @@ public override Task> RetrieveModels() Model = "claude-opus-5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, Rank = 9995, ContextLimit = 1000000, @@ -88,6 +99,15 @@ public override Task> RetrieveModels() InputCacheWrite = 0.00000625m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.0000125m, + InputCacheRead = 0.00000025m, + InputCacheWrite = 0.000003125m, + WebSearch = 0.01m, + }, + Aliases = new List { "claude-opus-5:batch" }, }, new AIModelCapabilities @@ -116,6 +136,7 @@ public override Task> RetrieveModels() Model = "claude-fable-5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, Rank = 9985, ContextLimit = 1000000, @@ -128,6 +149,15 @@ public override Task> RetrieveModels() InputCacheWrite = 0.0000125m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000005m, + Completion = 0.000025m, + InputCacheRead = 0.0000005m, + InputCacheWrite = 0.00000625m, + WebSearch = 0.01m, + }, + Aliases = new List { "claude-fable-5:batch" }, }, @@ -137,40 +167,44 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = providerName, - Model = "claude-sonnet-4-6", + Model = "claude-opus-4-7", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - Default = AICapability.Text2Json, SupportsStreaming = true, Verified = false, Rank = 9980, ContextLimit = 1000000, - Created = new DateTime(2026, 2, 17), + Created = new DateTime(2026, 4, 16), Pricing = new AIModelPricing { - Prompt = 0.000003m, - Completion = 0.000015m, - InputCacheRead = 0.0000003m, - InputCacheWrite = 0.00000375m, + Prompt = 0.000005m, + Completion = 0.000025m, + InputCacheRead = 0.0000005m, + InputCacheWrite = 0.00000625m, WebSearch = 0.01m, }, }, + + + // Released between November 2025 and February 2026 + new AIModelCapabilities { Provider = providerName, - Model = "claude-opus-4-7", + Model = "claude-sonnet-4-6", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Default = AICapability.Text2Json, SupportsStreaming = true, Verified = false, Rank = 9975, ContextLimit = 1000000, - Created = new DateTime(2026, 4, 16), + Created = new DateTime(2026, 2, 17), Pricing = new AIModelPricing { - Prompt = 0.000005m, - Completion = 0.000025m, - InputCacheRead = 0.0000005m, - InputCacheWrite = 0.00000625m, + Prompt = 0.000003m, + Completion = 0.000015m, + InputCacheRead = 0.0000003m, + InputCacheWrite = 0.00000375m, WebSearch = 0.01m, }, }, @@ -195,10 +229,6 @@ public override Task> RetrieveModels() }, }, - - - // Released between November 2025 and February 2026 - new AIModelCapabilities { Provider = providerName, diff --git a/src/SmartHopper.Providers.Anthropic/AnthropicProviderSettings.cs b/src/SmartHopper.Providers.Anthropic/AnthropicProviderSettings.cs index 1ac6ece25..4f16155a6 100644 --- a/src/SmartHopper.Providers.Anthropic/AnthropicProviderSettings.cs +++ b/src/SmartHopper.Providers.Anthropic/AnthropicProviderSettings.cs @@ -20,9 +20,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Dialogs; -using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Providers.Anthropic { @@ -149,7 +150,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Max Tokens must be greater than 0.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Max Tokens must be greater than 0.")); } return false; @@ -167,7 +168,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Temperature must be between 0.0 and 2.0.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Temperature must be between 0.0 and 2.0.")); } return false; @@ -182,7 +183,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Reasoning effort must be low, medium, high, xhigh, or max.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Reasoning effort must be low, medium, high, xhigh, or max.")); } return false; diff --git a/src/SmartHopper.Providers.Anthropic/SmartHopper.Providers.Anthropic.csproj b/src/SmartHopper.Providers.Anthropic/SmartHopper.Providers.Anthropic.csproj index 8ef0c17e7..3f02e3671 100644 --- a/src/SmartHopper.Providers.Anthropic/SmartHopper.Providers.Anthropic.csproj +++ b/src/SmartHopper.Providers.Anthropic/SmartHopper.Providers.Anthropic.csproj @@ -41,7 +41,7 @@ - + diff --git a/src/SmartHopper.Providers.DeepSeek/DeepSeekJsonSchemaAdapter.cs b/src/SmartHopper.Providers.DeepSeek/DeepSeekJsonSchemaAdapter.cs index c3a85ad2e..67bdd1219 100644 --- a/src/SmartHopper.Providers.DeepSeek/DeepSeekJsonSchemaAdapter.cs +++ b/src/SmartHopper.Providers.DeepSeek/DeepSeekJsonSchemaAdapter.cs @@ -20,7 +20,7 @@ using System.Diagnostics; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; namespace SmartHopper.Providers.DeepSeek { diff --git a/src/SmartHopper.Providers.DeepSeek/DeepSeekProvider.cs b/src/SmartHopper.Providers.DeepSeek/DeepSeekProvider.cs index 0c53d9c92..b067225ce 100644 --- a/src/SmartHopper.Providers.DeepSeek/DeepSeekProvider.cs +++ b/src/SmartHopper.Providers.DeepSeek/DeepSeekProvider.cs @@ -30,16 +30,16 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.JsonSchemas; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Streaming; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Streaming; namespace SmartHopper.Providers.DeepSeek { diff --git a/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderFactory.cs b/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderFactory.cs index 32bb129af..6cea550ef 100644 --- a/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderFactory.cs +++ b/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderFactory.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.DeepSeek { diff --git a/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderModels.cs b/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderModels.cs index 3029b4b4b..142841a76 100644 --- a/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderModels.cs +++ b/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderModels.cs @@ -19,8 +19,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.DeepSeek { @@ -48,6 +48,28 @@ public override Task> RetrieveModels() var models = new List { + // Released between May 2026 and August 2026 + + new AIModelCapabilities + { + Provider = provider, + Model = "deepseek-v4-flash-vision-exp", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + Verified = false, + Rank = 10000, + ContextLimit = 1048576, + Created = new DateTime(2026, 8, 21), + Pricing = new AIModelPricing + { + Prompt = 0.00000022m, + Completion = 0.00000066m, + InputCacheRead = 0.000000007m, + }, + }, + + + // Released between February 2026 and May 2026 new AIModelCapabilities @@ -58,14 +80,14 @@ public override Task> RetrieveModels() Default = AICapability.Text2Text | AICapability.ToolChat | AICapability.ReasoningChat | AICapability.ToolReasoningChat | AICapability.Text2Json, SupportsStreaming = true, Verified = false, - Rank = 10000, + Rank = 9995, ContextLimit = 1048576, Created = new DateTime(2026, 4, 24), Pricing = new AIModelPricing { - Prompt = 0.00000014m, - Completion = 0.00000028m, - InputCacheRead = 0.000000028m, + Prompt = 0.00000005306m, + Completion = 0.00000010612m, + InputCacheRead = 0.000000010612m, }, }, @@ -76,14 +98,14 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9995, + Rank = 9990, ContextLimit = 1048576, Created = new DateTime(2026, 4, 24), Pricing = new AIModelPricing { - Prompt = 0.000000435m, - Completion = 0.00000087m, - InputCacheRead = 0.000000003625m, + Prompt = 0.000000396894m, + Completion = 0.000000793788m, + InputCacheRead = 0.0000000330745m, }, }, diff --git a/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderSettings.cs b/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderSettings.cs index 3721764ef..5de72ff82 100644 --- a/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderSettings.cs +++ b/src/SmartHopper.Providers.DeepSeek/DeepSeekProviderSettings.cs @@ -20,9 +20,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Dialogs; -using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Providers.DeepSeek { @@ -174,7 +175,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Max tokens must be a positive number.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Max tokens must be a positive number.")); } return false; @@ -194,7 +195,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Temperature must be between 0.0 and 2.0.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Temperature must be between 0.0 and 2.0.")); } return false; @@ -214,7 +215,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Top P must be between 0.0 and 1.0.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Top P must be between 0.0 and 1.0.")); } return false; @@ -230,7 +231,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Reasoning Effort must be 'none', 'high', or 'max'.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Reasoning Effort must be 'none', 'high', or 'max'.")); } return false; diff --git a/src/SmartHopper.Providers.DeepSeek/SmartHopper.Providers.DeepSeek.csproj b/src/SmartHopper.Providers.DeepSeek/SmartHopper.Providers.DeepSeek.csproj index 678fa94de..3efb47671 100644 --- a/src/SmartHopper.Providers.DeepSeek/SmartHopper.Providers.DeepSeek.csproj +++ b/src/SmartHopper.Providers.DeepSeek/SmartHopper.Providers.DeepSeek.csproj @@ -36,7 +36,7 @@ - + diff --git a/src/SmartHopper.Providers.Gemini/GeminiProvider.Batch.cs b/src/SmartHopper.Providers.Gemini/GeminiProvider.Batch.cs index a7c554830..b3e9c8384 100644 --- a/src/SmartHopper.Providers.Gemini/GeminiProvider.Batch.cs +++ b/src/SmartHopper.Providers.Gemini/GeminiProvider.Batch.cs @@ -24,12 +24,12 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Providers.Gemini { diff --git a/src/SmartHopper.Providers.Gemini/GeminiProvider.Decode.cs b/src/SmartHopper.Providers.Gemini/GeminiProvider.Decode.cs index 50f7e0b8b..945fb1fc1 100644 --- a/src/SmartHopper.Providers.Gemini/GeminiProvider.Decode.cs +++ b/src/SmartHopper.Providers.Gemini/GeminiProvider.Decode.cs @@ -21,10 +21,10 @@ using System.Diagnostics; using System.Linq; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.Diagnostics; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.Diagnostics; namespace SmartHopper.Providers.Gemini { diff --git a/src/SmartHopper.Providers.Gemini/GeminiProvider.Streaming.cs b/src/SmartHopper.Providers.Gemini/GeminiProvider.Streaming.cs index 31483b8de..ab6345a95 100644 --- a/src/SmartHopper.Providers.Gemini/GeminiProvider.Streaming.cs +++ b/src/SmartHopper.Providers.Gemini/GeminiProvider.Streaming.cs @@ -25,12 +25,12 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Streaming; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Streaming; namespace SmartHopper.Providers.Gemini { diff --git a/src/SmartHopper.Providers.Gemini/GeminiProvider.cs b/src/SmartHopper.Providers.Gemini/GeminiProvider.cs index 67eed74c7..b70d16f24 100644 --- a/src/SmartHopper.Providers.Gemini/GeminiProvider.cs +++ b/src/SmartHopper.Providers.Gemini/GeminiProvider.cs @@ -28,14 +28,14 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; using SmartHopper.Providers.Gemini.Properties; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.Gemini { diff --git a/src/SmartHopper.Providers.Gemini/GeminiProviderFactory.cs b/src/SmartHopper.Providers.Gemini/GeminiProviderFactory.cs index ba5432410..011dcda1d 100644 --- a/src/SmartHopper.Providers.Gemini/GeminiProviderFactory.cs +++ b/src/SmartHopper.Providers.Gemini/GeminiProviderFactory.cs @@ -18,7 +18,7 @@ using System; using System.Diagnostics; -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.Gemini { diff --git a/src/SmartHopper.Providers.Gemini/GeminiProviderModels.cs b/src/SmartHopper.Providers.Gemini/GeminiProviderModels.cs index cb856f077..9d940d09c 100644 --- a/src/SmartHopper.Providers.Gemini/GeminiProviderModels.cs +++ b/src/SmartHopper.Providers.Gemini/GeminiProviderModels.cs @@ -21,11 +21,11 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.Gemini { @@ -54,24 +54,38 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = providerName, - Model = "gemini-3.1-flash-lite", + Model = "gemini-3.7-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, Rank = 10000, ContextLimit = 1048576, - Created = new DateTime(2026, 5, 7), + Created = new DateTime(2026, 8, 13), Pricing = new AIModelPricing { - Prompt = 0.00000025m, - Completion = 0.0000015m, - Image = 0.00000025m, - Audio = 0.0000005m, - InputAudioCache = 0.00000005m, - InputCacheRead = 0.000000025m, - InputCacheWrite = 0.00000008333333333333334m, - InternalReasoning = 0.0000015m, + Prompt = 0.000000375m, + Completion = 0.000001875m, + Image = 0.000000375m, + Audio = 0.000000375m, + InputAudioCache = 0.0000000375m, + InputCacheRead = 0.0000000375m, + InputCacheWrite = 0.0000000208333333333333m, + InternalReasoning = 0.000001875m, + WebSearch = 0.014m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000001875m, + Completion = 0.0000009375m, + Image = 0.0000001875m, + Audio = 0.0000001875m, + InputAudioCache = 0.00000001875m, + InputCacheRead = 0.00000001875m, + InputCacheWrite = 0.0000000208333333333333m, + InternalReasoning = 0.0000009375m, WebSearch = 0.014m, }, + Aliases = new List { "gemini-3.7-flash:batch" }, }, new AIModelCapabilities @@ -79,6 +93,7 @@ public override Task> RetrieveModels() Provider = providerName, Model = "gemini-3.5-flash-lite", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, Rank = 9995, ContextLimit = 1048576, @@ -91,10 +106,22 @@ public override Task> RetrieveModels() Audio = 0.0000003m, InputAudioCache = 0.00000003m, InputCacheRead = 0.00000003m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000025m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000015m, + Completion = 0.00000125m, + Image = 0.00000015m, + Audio = 0.00000015m, + InputAudioCache = 0.000000015m, + InputCacheRead = 0.000000015m, + InternalReasoning = 0.00000125m, + WebSearch = 0.014m, + }, + Aliases = new List { "gemini-3.5-flash-lite:batch" }, }, new AIModelCapabilities @@ -102,45 +129,36 @@ public override Task> RetrieveModels() Provider = providerName, Model = "gemini-3.6-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, Rank = 9990, ContextLimit = 1048576, Created = new DateTime(2026, 7, 21), Pricing = new AIModelPricing { - Prompt = 0.0000015m, - Completion = 0.0000075m, - Image = 0.0000015m, - Audio = 0.0000015m, - InputAudioCache = 0.00000015m, - InputCacheRead = 0.00000015m, - InputCacheWrite = 0.00000008333333333333334m, - InternalReasoning = 0.0000075m, + Prompt = 0.00000075m, + Completion = 0.00000375m, + Image = 0.00000075m, + Audio = 0.00000075m, + InputAudioCache = 0.000000075m, + InputCacheRead = 0.000000075m, + InputCacheWrite = 0.0000000416666666666667m, + InternalReasoning = 0.00000375m, WebSearch = 0.014m, }, - }, - - new AIModelCapabilities - { - Provider = providerName, - Model = "gemini-3.5-flash", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - Verified = false, - Rank = 9985, - ContextLimit = 1048576, - Created = new DateTime(2026, 5, 19), - Pricing = new AIModelPricing + BatchPricing = new AIModelPricing { - Prompt = 0.0000015m, - Completion = 0.000009m, - Image = 0.0000015m, - Audio = 0.000003m, - InputAudioCache = 0.0000003m, - InputCacheRead = 0.00000015m, - InputCacheWrite = 0.00000008333333333333334m, - InternalReasoning = 0.000009m, + Prompt = 0.000000375m, + Completion = 0.000001875m, + Image = 0.000000375m, + Audio = 0.000000375m, + InputAudioCache = 0.0000000375m, + InputCacheRead = 0.0000000375m, + InputCacheWrite = 0.0000000416666666666667m, + InternalReasoning = 0.000001875m, WebSearch = 0.014m, }, + Aliases = new List { "gemini-3.6-flash:batch" }, }, new AIModelCapabilities @@ -149,7 +167,7 @@ public override Task> RetrieveModels() Model = "gemini-3.1-flash-lite-image", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, Verified = false, - Rank = 9980, + Rank = 9985, ContextLimit = 65536, Created = new DateTime(2026, 6, 30), Pricing = new AIModelPricing @@ -167,7 +185,7 @@ public override Task> RetrieveModels() Model = "gemini-3.1-flash-image", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, Verified = false, - Rank = 9975, + Rank = 9980, ContextLimit = 131072, Created = new DateTime(2026, 6, 18), Pricing = new AIModelPricing @@ -185,7 +203,7 @@ public override Task> RetrieveModels() Model = "gemini-3-pro-image", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, Verified = false, - Rank = 9970, + Rank = 9975, ContextLimit = 131072, Created = new DateTime(2026, 6, 18), Pricing = new AIModelPricing @@ -213,7 +231,7 @@ public override Task> RetrieveModels() Model = "lyria-3-pro-preview", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.JsonOutput, Verified = false, - Rank = 9965, + Rank = 9970, ContextLimit = 1048576, Created = new DateTime(2026, 3, 30), }, @@ -224,7 +242,7 @@ public override Task> RetrieveModels() Model = "lyria-3-clip-preview", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.JsonOutput, Verified = false, - Rank = 9960, + Rank = 9965, ContextLimit = 1048576, Created = new DateTime(2026, 3, 30), }, @@ -235,7 +253,7 @@ public override Task> RetrieveModels() Model = "gemma-4-26b-a4b-it", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, Verified = false, - Rank = 9955, + Rank = 9960, ContextLimit = 262144, Created = new DateTime(2026, 4, 3), Pricing = new AIModelPricing @@ -252,7 +270,7 @@ public override Task> RetrieveModels() Model = "gemma-4-31b-it", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, Verified = false, - Rank = 9950, + Rank = 9955, ContextLimit = 262144, Created = new DateTime(2026, 4, 2), Pricing = new AIModelPricing @@ -263,6 +281,42 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = providerName, + Model = "gemini-3.1-flash-lite", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, + Verified = false, + Rank = 9950, + ContextLimit = 1048576, + Created = new DateTime(2026, 5, 7), + Pricing = new AIModelPricing + { + Prompt = 0.00000025m, + Completion = 0.0000015m, + Image = 0.00000025m, + Audio = 0.0000005m, + InputAudioCache = 0.00000005m, + InputCacheRead = 0.000000025m, + InputCacheWrite = 0.0000000833333333333333m, + InternalReasoning = 0.0000015m, + WebSearch = 0.014m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000125m, + Completion = 0.00000075m, + Image = 0.000000125m, + Audio = 0.00000025m, + InputAudioCache = 0.000000025m, + InputCacheRead = 0.0000000125m, + InternalReasoning = 0.00000075m, + WebSearch = 0.014m, + }, + Aliases = new List { "gemini-3.1-flash-lite:batch" }, + }, + new AIModelCapabilities { Provider = providerName, @@ -280,7 +334,7 @@ public override Task> RetrieveModels() Audio = 0.0000005m, InputAudioCache = 0.00000005m, InputCacheRead = 0.000000025m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000015m, WebSearch = 0.014m, }, @@ -289,36 +343,48 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = providerName, - Model = "gemini-3.1-pro-preview-customtools", + Model = "gemini-3.5-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, Rank = 9940, ContextLimit = 1048576, - Created = new DateTime(2026, 2, 25), + Created = new DateTime(2026, 5, 19), Pricing = new AIModelPricing { - Prompt = 0.000002m, - Completion = 0.000012m, - Image = 0.000002m, - Audio = 0.000002m, - InputAudioCache = 0.0000002m, - InputCacheRead = 0.0000002m, - InputCacheWrite = 0.000000375m, - InternalReasoning = 0.000012m, + Prompt = 0.0000015m, + Completion = 0.000009m, + Image = 0.0000015m, + Audio = 0.000003m, + InputAudioCache = 0.0000003m, + InputCacheRead = 0.00000015m, + InputCacheWrite = 0.0000000833333333333333m, + InternalReasoning = 0.000009m, + WebSearch = 0.014m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000075m, + Completion = 0.0000045m, + Image = 0.00000075m, + Audio = 0.0000015m, + InputAudioCache = 0.00000015m, + InputCacheRead = 0.000000075m, + InternalReasoning = 0.0000045m, WebSearch = 0.014m, }, + Aliases = new List { "gemini-3.5-flash:batch" }, }, new AIModelCapabilities { Provider = providerName, - Model = "gemini-3.1-pro-preview", + Model = "gemini-3.1-pro-preview-customtools", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, Verified = false, Rank = 9935, ContextLimit = 1048576, - Created = new DateTime(2026, 2, 19), + Created = new DateTime(2026, 2, 25), Pricing = new AIModelPricing { Prompt = 0.000002m, @@ -362,6 +428,7 @@ public override Task> RetrieveModels() Provider = providerName, Model = "gemini-3-flash-preview", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, Rank = 9925, ContextLimit = 1048576, @@ -374,29 +441,38 @@ public override Task> RetrieveModels() Audio = 0.000001m, InputAudioCache = 0.0000001m, InputCacheRead = 0.00000005m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.000003m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000025m, + Completion = 0.0000015m, + Image = 0.00000025m, + Audio = 0.0000005m, + InternalReasoning = 0.0000015m, + WebSearch = 0.014m, + }, + Aliases = new List { "gemini-3-flash-preview:batch" }, }, new AIModelCapabilities { Provider = providerName, - Model = "gemini-3-pro-image-preview", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, - Default = AICapability.Text2Image | AICapability.Image2Image, + Model = "gemini-3.1-pro-preview", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, Rank = 9920, - ContextLimit = 65536, - Created = new DateTime(2025, 11, 20), + ContextLimit = 1048576, + Created = new DateTime(2026, 2, 19), Pricing = new AIModelPricing { Prompt = 0.000002m, Completion = 0.000012m, Image = 0.000002m, - ImageOutput = 0.00012m, Audio = 0.000002m, InputAudioCache = 0.0000002m, InputCacheRead = 0.0000002m, @@ -404,6 +480,16 @@ public override Task> RetrieveModels() InternalReasoning = 0.000012m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000006m, + Image = 0.000001m, + Audio = 0.000001m, + InternalReasoning = 0.000006m, + WebSearch = 0.014m, + }, + Aliases = new List { "gemini-3.1-pro-preview:batch" }, }, @@ -430,12 +516,38 @@ public override Task> RetrieveModels() Audio = 0.000001m, InputAudioCache = 0.0000001m, InputCacheRead = 0.00000003m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000025m, WebSearch = 0.014m, }, }, + new AIModelCapabilities + { + Provider = providerName, + Model = "gemini-3-pro-image-preview", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, + Default = AICapability.Text2Image | AICapability.Image2Image, + SupportsStreaming = true, + Verified = false, + Rank = 9910, + ContextLimit = 65536, + Created = new DateTime(2025, 11, 20), + Pricing = new AIModelPricing + { + Prompt = 0.000002m, + Completion = 0.000012m, + Image = 0.000002m, + ImageOutput = 0.00012m, + Audio = 0.000002m, + InputAudioCache = 0.0000002m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.000000375m, + InternalReasoning = 0.000012m, + WebSearch = 0.014m, + }, + }, + // Released between May 2025 and August 2025 @@ -446,8 +558,9 @@ public override Task> RetrieveModels() Model = "gemini-2.5-flash-lite", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = true, - Rank = 9910, + Rank = 9905, ContextLimit = 1048576, Created = new DateTime(2025, 7, 22), Pricing = new AIModelPricing @@ -458,10 +571,22 @@ public override Task> RetrieveModels() Audio = 0.0000003m, InputAudioCache = 0.00000003m, InputCacheRead = 0.00000001m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000004m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000005m, + Completion = 0.0000002m, + Image = 0.00000005m, + Audio = 0.00000015m, + InputAudioCache = 0.00000003m, + InputCacheRead = 0.00000001m, + InternalReasoning = 0.0000002m, + WebSearch = 0.014m, + }, + Aliases = new List { "gemini-2.5-flash-lite:batch" }, }, new AIModelCapabilities @@ -471,8 +596,9 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, Default = AICapability.Text2Text | AICapability.Text2Json | AICapability.ReasoningChat | AICapability.ToolReasoningChat, SupportsStreaming = true, + SupportsBatch = true, Verified = true, - Rank = 9905, + Rank = 9900, ContextLimit = 1048576, Created = new DateTime(2025, 6, 17), Pricing = new AIModelPricing @@ -483,10 +609,22 @@ public override Task> RetrieveModels() Audio = 0.000001m, InputAudioCache = 0.0000001m, InputCacheRead = 0.00000003m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000025m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000015m, + Completion = 0.00000125m, + Image = 0.00000015m, + Audio = 0.0000005m, + InputAudioCache = 0.0000001m, + InputCacheRead = 0.00000003m, + InternalReasoning = 0.00000125m, + WebSearch = 0.014m, + }, + Aliases = new List { "gemini-2.5-flash:batch" }, }, new AIModelCapabilities @@ -495,8 +633,9 @@ public override Task> RetrieveModels() Model = "gemini-2.5-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = true, - Rank = 9900, + Rank = 9895, ContextLimit = 1048576, Created = new DateTime(2025, 6, 17), Pricing = new AIModelPricing @@ -511,22 +650,18 @@ public override Task> RetrieveModels() InternalReasoning = 0.00001m, WebSearch = 0.014m, }, - }, - - new AIModelCapabilities - { - Provider = providerName, - Model = "gemma-3n-e4b-it", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, - Verified = false, - Rank = 9895, - ContextLimit = 32768, - Created = new DateTime(2025, 5, 20), - Pricing = new AIModelPricing + BatchPricing = new AIModelPricing { - Prompt = 0.00000006m, - Completion = 0.00000012m, + Prompt = 0.000000625m, + Completion = 0.000005m, + Image = 0.000000625m, + Audio = 0.000000625m, + InputAudioCache = 0.000000125m, + InputCacheRead = 0.000000125m, + InternalReasoning = 0.000005m, + WebSearch = 0.014m, }, + Aliases = new List { "gemini-2.5-pro:batch" }, }, new AIModelCapabilities @@ -552,46 +687,39 @@ public override Task> RetrieveModels() }, }, + + + // Released between February 2025 and May 2025 + new AIModelCapabilities { Provider = providerName, - Model = "gemini-2.5-pro-preview-05-06", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "gemma-3-4b-it", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, Verified = false, Rank = 9885, - ContextLimit = 1048576, - Created = new DateTime(2025, 5, 7), + ContextLimit = 131072, + Created = new DateTime(2025, 3, 13), Pricing = new AIModelPricing { - Prompt = 0.00000125m, - Completion = 0.00001m, - Image = 0.00000125m, - Audio = 0.00000125m, - InputAudioCache = 0.000000125m, - InputCacheRead = 0.000000125m, - InputCacheWrite = 0.000000375m, - InternalReasoning = 0.00001m, - WebSearch = 0.014m, + Prompt = 0.00000005m, + Completion = 0.0000001m, }, }, - - - // Released between February 2025 and May 2025 - new AIModelCapabilities { Provider = providerName, - Model = "gemma-3-4b-it", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, + Model = "gemma-3n-e4b-it", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, Verified = false, Rank = 9880, - ContextLimit = 131072, - Created = new DateTime(2025, 3, 13), + ContextLimit = 32768, + Created = new DateTime(2025, 5, 20), Pricing = new AIModelPricing { - Prompt = 0.00000005m, - Completion = 0.0000001m, + Prompt = 0.00000006m, + Completion = 0.00000012m, }, }, @@ -628,6 +756,29 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = providerName, + Model = "gemini-2.5-pro-preview-05-06", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Verified = false, + Rank = 9865, + ContextLimit = 1048576, + Created = new DateTime(2025, 5, 7), + Pricing = new AIModelPricing + { + Prompt = 0.00000125m, + Completion = 0.00001m, + Image = 0.00000125m, + Audio = 0.00000125m, + InputAudioCache = 0.000000125m, + InputCacheRead = 0.000000125m, + InputCacheWrite = 0.000000375m, + InternalReasoning = 0.00001m, + WebSearch = 0.014m, + }, + }, + // Released before August 2024 or unknown release date @@ -638,7 +789,7 @@ public override Task> RetrieveModels() Model = "gemma-2-27b-it", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, Verified = false, - Rank = 9865, + Rank = 9860, ContextLimit = 8192, Created = new DateTime(2024, 7, 13), Pricing = new AIModelPricing diff --git a/src/SmartHopper.Providers.Gemini/GeminiProviderSettings.cs b/src/SmartHopper.Providers.Gemini/GeminiProviderSettings.cs index 3ddd97c19..da8a704b6 100644 --- a/src/SmartHopper.Providers.Gemini/GeminiProviderSettings.cs +++ b/src/SmartHopper.Providers.Gemini/GeminiProviderSettings.cs @@ -19,9 +19,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Dialogs; -using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Providers.Gemini { @@ -142,7 +143,7 @@ public override bool ValidateSettings(Dictionary settings) if (maxTokens <= 0) { - if (showErrorDialogs) StyledMessageDialog.ShowError("Max Tokens must be greater than 0.", "Validation Error"); + if (showErrorDialogs) ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Max Tokens must be greater than 0.")); return false; } } @@ -156,7 +157,7 @@ public override bool ValidateSettings(Dictionary settings) if (temperature < 0.0 || temperature > 2.0) { - if (showErrorDialogs) StyledMessageDialog.ShowError("Temperature must be between 0.0 and 2.0.", "Validation Error"); + if (showErrorDialogs) ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Temperature must be between 0.0 and 2.0.")); return false; } } diff --git a/src/SmartHopper.Providers.Gemini/SmartHopper.Providers.Gemini.csproj b/src/SmartHopper.Providers.Gemini/SmartHopper.Providers.Gemini.csproj index cd16933b6..e9febbc21 100644 --- a/src/SmartHopper.Providers.Gemini/SmartHopper.Providers.Gemini.csproj +++ b/src/SmartHopper.Providers.Gemini/SmartHopper.Providers.Gemini.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/SmartHopper.Providers.LocalAI/LocalAIJsonSchemaAdapter.cs b/src/SmartHopper.Providers.LocalAI/LocalAIJsonSchemaAdapter.cs new file mode 100644 index 000000000..a29826760 --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/LocalAIJsonSchemaAdapter.cs @@ -0,0 +1,91 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using Newtonsoft.Json.Linq; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; + +namespace SmartHopper.Providers.LocalAI +{ + /// + /// JSON schema adapter for LocalAI. + /// LocalAI follows the OpenAI schema requirements (object-root) when used with + /// the llama.cpp grammar-based JSON output. Non-object schemas are wrapped. + /// + internal sealed class LocalAIJsonSchemaAdapter : IJsonSchemaAdapter + { + /// + public string ProviderName => LocalAIProvider.NameValue; + + /// + public (JObject wrapped, SchemaWrapperInfo info) Wrap(JObject schema) + { + if (schema is null) + { + throw new ArgumentNullException(nameof(schema)); + } + + var schemaType = schema["type"]?.ToString(); + + if (string.Equals(schemaType, "object", StringComparison.OrdinalIgnoreCase)) + { + return (schema, new SchemaWrapperInfo { IsWrapped = false, ProviderName = this.ProviderName }); + } + + if (string.Equals(schemaType, "array", StringComparison.OrdinalIgnoreCase)) + { + var wrapped = new JObject + { + ["type"] = "object", + ["properties"] = new JObject { ["items"] = schema }, + ["required"] = new JArray { "items" }, + ["additionalProperties"] = false, + }; + return (wrapped, new SchemaWrapperInfo { IsWrapped = true, WrapperType = "array", PropertyName = "items", ProviderName = this.ProviderName }); + } + + if (schemaType == "string" || schemaType == "number" || schemaType == "integer" || schemaType == "boolean") + { + var wrapped = new JObject + { + ["type"] = "object", + ["properties"] = new JObject { ["value"] = schema }, + ["required"] = new JArray { "value" }, + ["additionalProperties"] = false, + }; + return (wrapped, new SchemaWrapperInfo { IsWrapped = true, WrapperType = schemaType ?? "primitive", PropertyName = "value", ProviderName = this.ProviderName }); + } + + var generic = new JObject + { + ["type"] = "object", + ["properties"] = new JObject { ["data"] = schema }, + ["required"] = new JArray { "data" }, + ["additionalProperties"] = false, + }; + return (generic, new SchemaWrapperInfo { IsWrapped = true, WrapperType = "unknown", PropertyName = "data", ProviderName = this.ProviderName }); + } + + /// + public string Unwrap(string content, SchemaWrapperInfo info) + { + // Default service logic handles unwrap for object-root payloads + return content; + } + } +} diff --git a/src/SmartHopper.Providers.LocalAI/LocalAIProvider.Streaming.cs b/src/SmartHopper.Providers.LocalAI/LocalAIProvider.Streaming.cs new file mode 100644 index 000000000..abc8af40f --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/LocalAIProvider.Streaming.cs @@ -0,0 +1,405 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Streaming; + +namespace SmartHopper.Providers.LocalAI +{ + /// + /// Streaming half of . + /// + public sealed partial class LocalAIProvider + { + /// + protected override IStreamingAdapter CreateStreamingAdapter() + { + return new LocalAIStreamingAdapter(this); + } + + /// + /// Provider-scoped streaming adapter for LocalAI Chat Completions SSE. + /// LocalAI follows the OpenAI streaming protocol (data: {...}\n\n + [DONE]). + /// + private sealed class LocalAIStreamingAdapter : AIProviderStreamingAdapter, IStreamingAdapter + { + public LocalAIStreamingAdapter(LocalAIProvider provider) + : base(provider) + { + } + + public async IAsyncEnumerable StreamAsync( + AIRequestCall request, + StreamingOptions options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (request == null) + { + yield break; + } + + request = this.Prepare(request); + + if (!string.Equals(request.Endpoint, "/chat/completions", StringComparison.Ordinal)) + { + var unsupported = new AIReturn(); + unsupported.CreateProviderError("Streaming is only supported for /chat/completions in this adapter.", request); + yield return unsupported; + yield break; + } + + JObject body; + try + { + body = JObject.Parse(this.Provider.Encode(request)); + } + catch + { + body = new JObject(); + } + + body["stream"] = true; + + var fullUrl = this.BuildFullUrl(request.Endpoint); + + using var httpClient = this.CreateHttpClient(); + AIReturn? authError = null; + try + { + var apiKey = ((LocalAIProvider)this.Provider).GetApiKey(); + this.ApplyAuthentication(httpClient, request.Authentication, apiKey); + } + catch (Exception ex) + { + authError = new AIReturn(); + authError.CreateProviderError(ex.Message, request); + } + + if (authError != null) + { + yield return authError; + yield break; + } + + using var httpRequest = this.CreateSsePost(fullUrl, body.ToString(), "application/json"); + + HttpResponseMessage response; + AIReturn? sendError = null; + try + { + response = await this.SendForStreamAsync(httpClient, httpRequest, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + sendError = new AIReturn(); + sendError.CreateNetworkError(ex.InnerException?.Message ?? ex.Message, request); + response = null!; + } + + if (sendError != null) + { + yield return sendError; + yield break; + } + + if (!response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + var (message, isNetworkLike) = AIProvider.ClassifyHttpError((int)response.StatusCode, response.ReasonPhrase, content, this.Provider.Name); + var err = new AIReturn(); + if (isNetworkLike) + { + err.CreateNetworkError(message, request); + } + else + { + err.CreateProviderError(message, request); + } + + yield return err; + yield break; + } + + var buffer = new StringBuilder(); + var lastEmit = DateTime.UtcNow; + var firstChunk = true; + string finalFinishReason = string.Empty; + int promptTokens = 0; + int completionTokens = 0; + + var assistantAggregate = new AIInteractionText + { + Agent = AIAgent.Assistant, + Content = string.Empty, + Metrics = new AIMetrics { Provider = this.Provider.Name, Model = request.Model }, + }; + + // Tool call accumulation (index -> partial) + var toolCalls = new Dictionary(); + + async IAsyncEnumerable EmitAsync(string text, bool streamingStatus) + { + if (string.IsNullOrEmpty(text)) yield break; + + assistantAggregate.AppendDelta(contentDelta: text); + + var snapshot = new AIInteractionText + { + Agent = assistantAggregate.Agent, + Content = assistantAggregate.Content, + Metrics = new AIMetrics + { + Provider = assistantAggregate.Metrics.Provider, + Model = assistantAggregate.Metrics.Model, + FinishReason = assistantAggregate.Metrics.FinishReason, + InputTokensCached = assistantAggregate.Metrics.InputTokensCached, + InputTokensPrompt = assistantAggregate.Metrics.InputTokensPrompt, + OutputTokensReasoning = assistantAggregate.Metrics.OutputTokensReasoning, + OutputTokensGeneration = assistantAggregate.Metrics.OutputTokensGeneration, + CompletionTime = assistantAggregate.Metrics.CompletionTime, + }, + }; + + var delta = new AIReturn + { + Request = request, + Status = streamingStatus ? AICallStatus.Streaming : AICallStatus.Processing, + }; + delta.SetBody(new List { snapshot }); + yield return delta; + await Task.Yield(); + } + + async Task> FlushAsync(bool force) + { + var results = new List(); + if (buffer.Length == 0) return results; + var elapsed = (DateTime.UtcNow - lastEmit).TotalMilliseconds; + if (force || !options.CoalesceTokens || buffer.Length >= options.PreferredChunkSize || elapsed >= options.CoalesceDelayMs) + { + var text = buffer.ToString(); + buffer.Clear(); + lastEmit = DateTime.UtcNow; + await foreach (var d in EmitAsync(text, streamingStatus: true).WithCancellation(cancellationToken)) + { + results.Add(d); + } + } + + return results; + } + + { + var initial = new AIReturn { Request = request, Status = AICallStatus.Processing }; + initial.SetBody(new List()); + yield return initial; + } + + var idleTimeout = TimeSpan.FromSeconds((double)(request.TimeoutSeconds > 0 ? request.TimeoutSeconds : TimeoutDefaults.DefaultTimeoutSeconds)); + await foreach (var data in this.ReadSseDataAsync( + response, + idleTimeout, + null, + cancellationToken).WithCancellation(cancellationToken)) + { + JObject parsed; + try + { + parsed = JObject.Parse(data); + } + catch + { + continue; + } + + var choices = parsed["choices"] as JArray; + var choice = choices?.FirstOrDefault() as JObject; + var delta = choice?["delta"] as JObject; + var finishReason = choice?["finish_reason"]?.ToString(); + bool hasFinish = !string.IsNullOrEmpty(finishReason); + if (hasFinish) finalFinishReason = finishReason; + + var usage = parsed["usage"] as JObject; + if (usage != null) + { + var pt = usage["prompt_tokens"]?.Value(); + var ct = usage["completion_tokens"]?.Value(); + if (pt.HasValue) promptTokens = pt.Value; + if (ct.HasValue) completionTokens = ct.Value; + + assistantAggregate.AppendDelta(metricsDelta: new AIMetrics + { + Provider = this.Provider.Name, + Model = request.Model, + InputTokensPrompt = pt ?? 0, + OutputTokensGeneration = ct ?? 0, + }); + } + + var contentDelta = delta?["content"]?.ToString(); + if (!string.IsNullOrEmpty(contentDelta)) + { + buffer.Append(contentDelta); + var emitted = await FlushAsync(force: firstChunk).ConfigureAwait(false); + if (firstChunk) firstChunk = false; + foreach (var d in emitted) + { + yield return d; + } + } + + var tcArray = delta?["tool_calls"] as JArray; + if (tcArray != null) + { + foreach (var t in tcArray.OfType()) + { + var idx = t["index"]?.Value() ?? 0; + if (!toolCalls.TryGetValue(idx, out var entry)) + { + entry = (Id: string.Empty, Name: string.Empty, Args: new StringBuilder()); + toolCalls[idx] = entry; + } + + var idVal = t["id"]?.ToString(); + if (!string.IsNullOrEmpty(idVal)) entry.Id = idVal; + + var func = t["function"] as JObject; + if (func != null) + { + var name = func["name"]?.ToString(); + if (!string.IsNullOrEmpty(name)) entry.Name = name; + var args = func["arguments"]?.ToString(); + if (!string.IsNullOrEmpty(args)) entry.Args.Append(args); + } + + toolCalls[idx] = entry; + } + + var emittedTc = await FlushAsync(force: true).ConfigureAwait(false); + foreach (var d in emittedTc) + { + yield return d; + } + + var interactions = new List(); + foreach (var kv in toolCalls.OrderBy(k => k.Key)) + { + var (id, name, argsSb) = kv.Value; + JObject? argsObj = null; + var argsStr = argsSb.ToString(); + try + { + if (!string.IsNullOrWhiteSpace(argsStr)) + { + argsObj = JObject.Parse(argsStr); + } + } + catch + { + // Partial JSON, ignore + } + + interactions.Add(new AIInteractionToolCall { Id = id, Name = name, Arguments = argsObj }); + } + + var tcDelta = new AIReturn { Request = request, Status = AICallStatus.CallingTools }; + tcDelta.SetBody(interactions); + yield return tcDelta; + } + } + + var finalEmitted = await FlushAsync(force: true).ConfigureAwait(false); + foreach (var d in finalEmitted) + { + yield return d; + } + + var final = new AIReturn + { + Request = request, + Status = AICallStatus.Finished, + }; + + assistantAggregate.AppendDelta(metricsDelta: new AIMetrics + { + FinishReason = string.IsNullOrEmpty(finalFinishReason) ? "stop" : finalFinishReason, + }); + + var finalBuilder = AIBodyBuilder.Create(); + if (!string.IsNullOrEmpty(assistantAggregate.Content)) + { + var finalSnapshot = new AIInteractionText + { + Agent = assistantAggregate.Agent, + Content = assistantAggregate.Content, + Metrics = new AIMetrics + { + Provider = assistantAggregate.Metrics.Provider, + Model = assistantAggregate.Metrics.Model, + FinishReason = assistantAggregate.Metrics.FinishReason, + InputTokensCached = assistantAggregate.Metrics.InputTokensCached, + InputTokensPrompt = assistantAggregate.Metrics.InputTokensPrompt, + OutputTokensReasoning = assistantAggregate.Metrics.OutputTokensReasoning, + OutputTokensGeneration = assistantAggregate.Metrics.OutputTokensGeneration, + CompletionTime = assistantAggregate.Metrics.CompletionTime, + }, + }; + finalBuilder.Add(finalSnapshot, markAsNew: false); + } + + foreach (var kv in toolCalls.OrderBy(k => k.Key)) + { + var (id, name, argsSb) = kv.Value; + JObject? argsObj = null; + var argsStr = argsSb.ToString(); + try + { + if (!string.IsNullOrWhiteSpace(argsStr)) + { + argsObj = JObject.Parse(argsStr); + } + } + catch + { + // Partial JSON + } + + finalBuilder.Add(new AIInteractionToolCall { Id = id, Name = name, Arguments = argsObj }, markAsNew: false); + } + + final.SetBody(finalBuilder.Build()); + yield return final; + } + } + } +} diff --git a/src/SmartHopper.Providers.LocalAI/LocalAIProvider.cs b/src/SmartHopper.Providers.LocalAI/LocalAIProvider.cs new file mode 100644 index 000000000..516bc85f2 --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/LocalAIProvider.cs @@ -0,0 +1,574 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Streaming; + +namespace SmartHopper.Providers.LocalAI +{ + /// + /// LocalAI provider implementation. + /// Targets self-hosted LocalAI instances (https://localai.io/) through the + /// OpenAI-compatible /v1/chat/completions endpoint. + /// + public sealed partial class LocalAIProvider : AIProvider + { + /// + /// The name of the provider. Used by the UI for provider selection. + /// + public static readonly string NameValue = "LocalAI"; + + /// + /// Fallback default server URL when no Base URL has been configured. + /// LocalAI defaults to port 8080 on localhost. + /// + public static readonly Uri FallbackServerUrl = new ("http://localhost:8080/v1"); + + /// + /// Initializes a new instance of the class. + /// Private constructor to enforce singleton pattern. + /// + private LocalAIProvider() + { + this.Models = new LocalAIProviderModels(this); + + // Register provider-specific JSON schema adapter (object-root wrapper, OpenAI-style) + JsonSchemaAdapterRegistry.Register(new LocalAIJsonSchemaAdapter()); + } + + /// + public override string Name => NameValue; + + /// + /// Gets the server URL configured for this provider. + /// Reads the user-provided ServerUrl setting and falls back to + /// when the setting is empty or not a valid absolute HTTP(S) URL. + /// + public override Uri DefaultServerUrl + { + get + { + var configured = this.GetServerUrlSetting(); + if (!string.IsNullOrWhiteSpace(configured) + && Uri.TryCreate(configured, UriKind.Absolute, out var uri) + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + { + return uri; + } + + return FallbackServerUrl; + } + } + + /// + public override bool IsEnabled => true; + + /// + /// LocalAI is configured when a Base URL is available. + /// The descriptor supplies a default loopback URL, so the provider is considered + /// configured out of the box unless the URL has been explicitly cleared. + /// + public override bool IsConfigured => !string.IsNullOrWhiteSpace(this.GetServerUrlSetting()); + + /// + /// Helper to retrieve the configured API key for this provider. + /// Exposed to the nested streaming adapter to avoid protected access issues. + /// + internal string GetApiKey() + { + return this.GetSetting("ApiKey"); + } + + /// + /// Helper to retrieve the configured Base URL setting (raw string). + /// + internal string GetServerUrlSetting() + { + return this.GetSetting("ServerUrl"); + } + + /// + public override Image Icon + { + get + { + var iconBytes = Properties.Resources.localai_icon; + using (var ms = new MemoryStream(iconBytes)) + { + return new Bitmap(ms); + } + } + } + + /// + public override AIRequestCall PreCall(AIRequestCall request) + { + // First do the base PreCall + request = base.PreCall(request); + + // Setup HTTP method, content type, and authentication. + request.HttpMethod = "POST"; + request.ContentType = "application/json"; + + // LocalAI API keys are optional (typically configured server-side). + // Use bearer auth only when the user has supplied a key; otherwise no auth. + var apiKey = this.GetApiKey(); + request.Authentication = string.IsNullOrWhiteSpace(apiKey) ? "none" : "bearer"; + + // LocalAI exposes the OpenAI-compatible chat completions endpoint + request.Endpoint = "/chat/completions"; + + return request; + } + + /// + public override string Encode(AIRequestCall request) + { + if (request.HttpMethod == "GET" || request.HttpMethod == "DELETE") + { + return "GET and DELETE requests do not use a request body"; + } + + var p = request.Parameters; + + int maxTokens = p?.MaxTokens ?? this.GetSetting("MaxTokens"); + double temperature = p?.Temperature ?? this.GetSetting("Temperature"); + string? toolFilter = request.Body.ToolFilter; + + Debug.WriteLine($"[LocalAI] Encode - Model: {request.Model}, MaxTokens: {maxTokens}"); + + // Simple sequential encoding (same approach as OpenAI/MistralAI/DeepSeek providers). + var convertedMessages = new JArray(); + + // Merge System and Summary interactions before encoding + var mergedInteractions = this.MergeSystemAndSummary(request.Body.Interactions); + + foreach (var interaction in mergedInteractions) + { + try + { + var token = this.EncodeToJToken(interaction); + if (token == null) + { + continue; + } + + var role = token["role"]?.ToString(); + if (string.IsNullOrEmpty(role)) + { + continue; + } + + convertedMessages.Add(token); + } + catch (Exception ex) + { + Debug.WriteLine($"[LocalAI] Warning: Could not encode interaction: {ex.Message}"); + } + } + + // Build request body + var requestBody = new JObject + { + ["model"] = request.Model, + ["messages"] = convertedMessages, + ["max_tokens"] = maxTokens, + ["temperature"] = temperature, + }; + + // Apply optional parameters from extras only + if (p?.Extras != null) + { + if (p.Extras.TryGetValue("top_p", out var topPToken) && topPToken != null) + { + requestBody["top_p"] = topPToken.Value(); + } + + if (p.Extras.TryGetValue("top_k", out var topKToken) && topKToken != null) + { + requestBody["top_k"] = topKToken.Value(); + } + + if (p.Extras.TryGetValue("presence_penalty", out var ppToken) && ppToken != null) + { + requestBody["presence_penalty"] = ppToken; + } + + if (p.Extras.TryGetValue("frequency_penalty", out var fpToken) && fpToken != null) + { + requestBody["frequency_penalty"] = fpToken; + } + } + + // Add JSON response format if schema is provided (centralized wrapping) + if (!string.IsNullOrWhiteSpace(request.Body.JsonOutputSchema)) + { + try + { + var svc = JsonSchemaService.Instance; + var schemaObj = JObject.Parse(request.Body.JsonOutputSchema); + var (wrappedSchema, wrapperInfo) = svc.WrapForProvider(schemaObj, this.Name); + svc.SetCurrentWrapperInfo(wrapperInfo); + Debug.WriteLine($"[LocalAI] Schema wrapper info stored (central): IsWrapped={wrapperInfo.IsWrapped}, Type={wrapperInfo.WrapperType}, Property={wrapperInfo.PropertyName}"); + + // LocalAI llama.cpp backends honor json_object response_format + requestBody["response_format"] = new JObject { ["type"] = "json_object" }; + + // Add a system guidance message including the wrapped schema + var systemMessage = new JObject + { + ["role"] = "system", + ["content"] = "The response must be a valid JSON object that strictly follows this schema: " + wrappedSchema.ToString(Newtonsoft.Json.Formatting.None), + }; + convertedMessages.Insert(0, systemMessage); + } + catch (Exception ex) + { + Debug.WriteLine($"[LocalAI] Failed to parse/handle JSON schema: {ex.Message}"); + JsonSchemaService.Instance.SetCurrentWrapperInfo(new SchemaWrapperInfo { IsWrapped = false }); + requestBody["response_format"] = new JObject { ["type"] = "text" }; + } + } + else + { + JsonSchemaService.Instance.SetCurrentWrapperInfo(new SchemaWrapperInfo { IsWrapped = false }); + requestBody["response_format"] = new JObject { ["type"] = "text" }; + } + + // Add tools if requested + if (!string.IsNullOrWhiteSpace(toolFilter)) + { + var tools = this.GetFormattedTools(toolFilter); + if (tools != null && tools.Count > 0) + { + requestBody["tools"] = tools; + + // Forced tool call uses OpenAI-compatible tool_choice with function name + if (request.ForceToolCall && !string.IsNullOrWhiteSpace(request.ForceToolName)) + { + requestBody["tool_choice"] = new JObject + { + ["type"] = "function", + ["function"] = new JObject { ["name"] = request.ForceToolName, }, + }; + Debug.WriteLine($"[LocalAI] Forcing tool call: {request.ForceToolName}"); + } + else + { + requestBody["tool_choice"] = "auto"; + } + } + } + + return requestBody.ToString(); + } + + /// + public override string Encode(IAIInteraction interaction) + { + try + { + var token = this.EncodeToJToken(interaction); + return token?.ToString() ?? string.Empty; + } + catch (Exception ex) + { + Debug.WriteLine($"[LocalAI] Encode(IAIInteraction) error: {ex.Message}"); + return string.Empty; + } + } + + /// + /// Converts a single interaction into a LocalAI/OpenAI-compatible message object. + /// Returns null for interactions that should not be sent (e.g., UI-only diagnostics). + /// + private JToken? EncodeToJToken(IAIInteraction interaction) + { + if (interaction == null) + { + return null; + } + + // UI-only diagnostics must not be sent to providers + if (interaction is AIInteractionRuntimeMessage) + { + return null; + } + + // Map role + string role; + switch (interaction.Agent) + { + case AIAgent.System: + case AIAgent.Context: + role = "system"; + break; + case AIAgent.User: + role = "user"; + break; + case AIAgent.Assistant: + case AIAgent.ToolCall: + role = "assistant"; + break; + case AIAgent.ToolResult: + role = "tool"; + break; + default: + return null; + } + + var messageObj = new JObject { ["role"] = role }; + + if (interaction is AIInteractionText textInteraction) + { + messageObj["content"] = textInteraction.Content ?? string.Empty; + } + else if (interaction is AIInteractionToolResult toolResultInteraction) + { + messageObj["content"] = toolResultInteraction.Result != null + ? JsonConvert.SerializeObject(toolResultInteraction.Result, Formatting.None) + : string.Empty; + + if (!string.IsNullOrWhiteSpace(toolResultInteraction.Id)) + { + messageObj["tool_call_id"] = toolResultInteraction.Id; + } + + if (!string.IsNullOrWhiteSpace(toolResultInteraction.Name)) + { + messageObj["name"] = toolResultInteraction.Name; + } + } + else if (interaction is AIInteractionToolCall toolCallInteraction) + { + var toolCallObj = new JObject + { + ["id"] = toolCallInteraction.Id ?? string.Empty, + ["type"] = "function", + ["function"] = new JObject + { + ["name"] = toolCallInteraction.Name ?? string.Empty, + ["arguments"] = toolCallInteraction.Arguments?.ToString() ?? "{}", + }, + }; + messageObj["tool_calls"] = new JArray { toolCallObj }; + messageObj["content"] = string.Empty; + } + else if (interaction is AIInteractionImage imageInteraction) + { + // LocalAI vision support depends on backend; fall back to the original prompt + messageObj["content"] = imageInteraction.OriginalPrompt ?? string.Empty; + } + else + { + messageObj["content"] = string.Empty; + } + + return messageObj; + } + + /// + public override List Decode(JObject response) + { + var interactions = new List(); + if (response == null) + { + return interactions; + } + + try + { + // OpenAI-compatible error envelope: {"error": {"message": "...", "type": "..."}} + if (response["error"] is JObject errorObj) + { + var errMsg = errorObj["message"]?.ToString() + ?? errorObj["type"]?.ToString() + ?? "Provider returned an error"; + Debug.WriteLine($"[LocalAI] Decode: provider error in response body: {errMsg}"); + interactions.Add(new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Error, Content = errMsg }); + return interactions; + } + + var choices = response["choices"] as JArray; + var firstChoice = choices?.FirstOrDefault() as JObject; + var message = firstChoice?["message"] as JObject; + if (message == null) + { + Debug.WriteLine("[LocalAI] Decode: No message found in response"); + return interactions; + } + + // Robust content extraction: handle string, array of parts, or object content + string content; + var contentToken = message["content"]; + if (contentToken is JArray contentParts && contentParts.Count > 0) + { + var texts = new List(); + foreach (var part in contentParts) + { + var text = part?["text"]?.ToString() + ?? part?["content"]?.ToString() + ?? part?.ToString(Newtonsoft.Json.Formatting.None) + ?? string.Empty; + if (!string.IsNullOrWhiteSpace(text)) + { + texts.Add(text); + } + } + + content = string.Join("\n", texts); + } + else + { + content = contentToken?.ToString() ?? string.Empty; + } + + // Apply centralized unwrapping using stored wrapper info + var wrapperInfo = JsonSchemaService.Instance.GetCurrentWrapperInfo(); + if (wrapperInfo != null && wrapperInfo.IsWrapped) + { + Debug.WriteLine($"[LocalAI] Unwrapping response content using wrapper info (central): Type={wrapperInfo.WrapperType}, Property={wrapperInfo.PropertyName}"); + content = JsonSchemaService.Instance.Unwrap(content, wrapperInfo); + } + + // LocalAI may also surface reasoning text via reasoning_content (see openai-functions docs) + var reasoning = message["reasoning_content"]?.ToString(); + + var tcs = message["tool_calls"] as JArray; + + // Avoid emitting an empty assistant text interaction for pure tool-call responses + if (!string.IsNullOrEmpty(content) + || !string.IsNullOrEmpty(reasoning) + || (tcs != null && tcs.Count > 0)) + { + var interaction = new AIInteractionText(); + interaction.SetResult( + agent: AIAgent.Assistant, + content: content, + reasoning: string.IsNullOrWhiteSpace(reasoning) ? null : reasoning); + interaction.Metrics = this.DecodeMetrics(response); + interactions.Add(interaction); + } + + // Add an AIInteractionToolCall for each tool call + if (tcs != null && tcs.Count > 0) + { + foreach (JObject tc in tcs.OfType()) + { + var function = tc["function"] as JObject; + var argumentsStr = function?["arguments"]?.ToString() ?? "{}"; + JObject? argumentsObj = null; + try + { + if (!string.IsNullOrWhiteSpace(argumentsStr)) + { + argumentsObj = JObject.Parse(argumentsStr); + } + } + catch + { + // Some local backends emit non-JSON arguments; pass them through as-is + } + + var toolCall = new AIInteractionToolCall + { + Id = tc["id"]?.ToString(), + Name = function?["name"]?.ToString(), + Arguments = argumentsObj, + Reasoning = string.IsNullOrWhiteSpace(reasoning) ? null : reasoning, + }; + interactions.Add(toolCall); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"[LocalAI] Decode error: {ex.Message}"); + } + + return interactions; + } + + /// + /// Decodes the metrics from the response (token usage + finish reason). + /// + private AIMetrics DecodeMetrics(JObject response) + { + var choices = response["choices"] as JArray; + var firstChoice = choices?.FirstOrDefault() as JObject; + var usage = response["usage"] as JObject; + + var metrics = new AIMetrics + { + Provider = this.Name, + FinishReason = firstChoice?["finish_reason"]?.ToString() ?? string.Empty, + InputTokensPrompt = usage?["prompt_tokens"]?.Value() ?? 0, + OutputTokensGeneration = usage?["completion_tokens"]?.Value() ?? 0, + }; + + return metrics; + } + + /// + public override IEnumerable GetExtraDescriptors() + { + return new[] + { + new AIExtraDescriptor( + "top_p", + "Top P", + "Nucleus sampling parameter (0.0-1.0). Lower values make output more focused; higher values more diverse. Leave empty to use the backend default.", + typeof(double), + null), + new AIExtraDescriptor( + "top_k", + "Top K", + "Limits the next-token candidate pool to the K most likely tokens. Supported by most llama.cpp backends. Leave empty to use the backend default.", + typeof(int), + null), + new AIExtraDescriptor( + "presence_penalty", + "Presence Penalty", + "Penalizes tokens already present in the text (-2.0 to 2.0). Positive values encourage new topics.", + typeof(double), + null), + new AIExtraDescriptor( + "frequency_penalty", + "Frequency Penalty", + "Penalizes frequent tokens (-2.0 to 2.0). Positive values reduce repetition.", + typeof(double), + null), + }; + } + } +} diff --git a/src/SmartHopper.Providers.LocalAI/LocalAIProviderFactory.cs b/src/SmartHopper.Providers.LocalAI/LocalAIProviderFactory.cs new file mode 100644 index 000000000..282f61254 --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/LocalAIProviderFactory.cs @@ -0,0 +1,41 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using SmartHopper.ProviderSdk.AIProviders; + +namespace SmartHopper.Providers.LocalAI +{ + /// + /// Factory class for creating instances of the LocalAI provider and its settings. + /// This class is discovered by the ProviderManager through reflection. + /// + public class LocalAIProviderFactory : IAIProviderFactory + { + /// + public IAIProvider CreateProvider() + { + return LocalAIProvider.Instance; + } + + /// + public IAIProviderSettings CreateProviderSettings() + { + return new LocalAIProviderSettings(LocalAIProvider.Instance); + } + } +} diff --git a/src/SmartHopper.Providers.LocalAI/LocalAIProviderModels.cs b/src/SmartHopper.Providers.LocalAI/LocalAIProviderModels.cs new file mode 100644 index 000000000..8f06d5e88 --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/LocalAIProviderModels.cs @@ -0,0 +1,53 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System.Collections.Generic; +using System.Threading.Tasks; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; + +namespace SmartHopper.Providers.LocalAI +{ + /// + /// LocalAI provider-specific model management implementation. + /// + /// + /// LocalAI is self-hosted and the available models depend entirely on what the + /// user has installed on their instance. We therefore do not ship a static catalog + /// of curated models. The user supplies the model name through the provider's + /// Model setting; we return an empty list and rely on the user-supplied name. + /// + public class LocalAIProviderModels : AIProviderModels + { + /// + /// Initializes a new instance of the class. + /// + /// The LocalAI provider instance. + public LocalAIProviderModels(LocalAIProvider provider) + : base(provider) + { + } + + /// + public override Task> RetrieveModels() + { + // Models are user-managed in LocalAI; no static catalog. + return Task.FromResult(new List()); + } + } +} diff --git a/src/SmartHopper.Providers.LocalAI/LocalAIProviderSettings.cs b/src/SmartHopper.Providers.LocalAI/LocalAIProviderSettings.cs new file mode 100644 index 000000000..122a7a266 --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/LocalAIProviderSettings.cs @@ -0,0 +1,174 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; + +namespace SmartHopper.Providers.LocalAI +{ + /// + /// Settings implementation for the LocalAI provider. + /// Exposes a configurable Base URL pointing at a self-hosted LocalAI instance, + /// plus an optional API key for installations that have authentication enabled. + /// + public class LocalAIProviderSettings : AIProviderSettings + { + private new readonly IAIProvider provider; + + /// + /// Initializes a new instance of the class. + /// + /// The provider associated with these settings. + public LocalAIProviderSettings(IAIProvider provider) + : base(provider) + { + this.provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + /// + public override IEnumerable GetSettingDescriptors() + { + return new[] + { + new SettingDescriptor + { + Name = "ServerUrl", + DisplayName = "Base URL", + Description = "Base URL of your LocalAI server, including the API version prefix (e.g. http://localhost:8080/v1).", + Type = typeof(string), + DefaultValue = LocalAIProvider.FallbackServerUrl.ToString(), + }, + new SettingDescriptor + { + Name = "ApiKey", + DisplayName = "API Key", + Description = "Optional API key for LocalAI installations that have authentication enabled. Leave empty for unauthenticated local instances.", + IsSecret = true, + Type = typeof(string), + }, + new SettingDescriptor + { + Name = "Model", + DisplayName = "Model", + Description = "Model name as registered in your LocalAI instance (e.g. 'gpt-3.5-turbo', 'llama-3-8b-instruct').", + Type = typeof(string), + }.Apply(d => d.SetLazyDefault(() => this.provider.GetDefaultModel())), + new SettingDescriptor + { + Name = "EnableStreaming", + Type = typeof(bool), + DefaultValue = true, + DisplayName = "Enable Streaming", + Description = "Allow streaming responses for this provider. When enabled, you will receive the response as it is generated.", + }, + new SettingDescriptor + { + Name = "MaxTokens", + DisplayName = "Max Tokens", + Description = "Maximum number of tokens to generate.", + Type = typeof(int), + DefaultValue = 2000, + ControlParams = new NumericSettingDescriptorControl + { + UseSlider = false, + Min = 1, + Max = 32768, + Step = 1, + }, + }, + new SettingDescriptor + { + Name = "Temperature", + Type = typeof(string), + DefaultValue = "0.5", + DisplayName = "Temperature", + Description = "Controls randomness (0.0-2.0). Higher values produce more diverse output; lower values are more focused and deterministic.", + }, + }; + } + + /// + public override bool ValidateSettings(Dictionary settings) + { + Debug.WriteLine($"[LocalAI] ValidateSettings called. Settings null? {settings == null}"); + + if (settings == null) + { + return false; + } + + const bool showErrorDialogs = true; + + // Validate Base URL when supplied: must parse as an absolute HTTP(S) URI. + if (settings.TryGetValue("ServerUrl", out var serverUrlObj) && serverUrlObj != null) + { + var serverUrl = serverUrlObj.ToString(); + if (!string.IsNullOrWhiteSpace(serverUrl)) + { + if (!Uri.TryCreate(serverUrl, UriKind.Absolute, out var parsed) + || (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps)) + { + if (showErrorDialogs) + { + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Base URL must be an absolute HTTP or HTTPS URL (for example http://localhost:8080/v1).")); + } + + return false; + } + } + } + + // MaxTokens must be a positive integer + if (settings.TryGetValue("MaxTokens", out var maxTokensObj) && maxTokensObj != null) + { + if (!int.TryParse(maxTokensObj.ToString(), out var parsedMaxTokens) || parsedMaxTokens <= 0) + { + if (showErrorDialogs) + { + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Max tokens must be a positive number.")); + } + + return false; + } + } + + // Temperature must be in [0.0, 2.0] + if (settings.TryGetValue("Temperature", out var temperatureObj) && temperatureObj != null) + { + if (!double.TryParse(temperatureObj.ToString(), out var parsedTemperature) + || parsedTemperature < 0.0 + || parsedTemperature > 2.0) + { + if (showErrorDialogs) + { + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Temperature must be between 0.0 and 2.0.")); + } + + return false; + } + } + + return true; + } + } +} diff --git a/src/SmartHopper.Providers.LocalAI/Properties/Resources.Designer.cs b/src/SmartHopper.Providers.LocalAI/Properties/Resources.Designer.cs new file mode 100644 index 000000000..c909308c9 --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/Properties/Resources.Designer.cs @@ -0,0 +1,75 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SmartHopper.Providers.LocalAI.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SmartHopper.Providers.LocalAI.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] localai_icon { + get { + object obj = ResourceManager.GetObject("localai_icon", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/src/SmartHopper.Providers.LocalAI/Properties/Resources.resx b/src/SmartHopper.Providers.LocalAI/Properties/Resources.resx new file mode 100644 index 000000000..6f27e6bb2 --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\localai_icon.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/src/SmartHopper.Providers.LocalAI/Resources/localai_icon.png b/src/SmartHopper.Providers.LocalAI/Resources/localai_icon.png new file mode 100644 index 000000000..4ef47b357 Binary files /dev/null and b/src/SmartHopper.Providers.LocalAI/Resources/localai_icon.png differ diff --git a/src/SmartHopper.Providers.LocalAI/SmartHopper.Providers.LocalAI.csproj b/src/SmartHopper.Providers.LocalAI/SmartHopper.Providers.LocalAI.csproj new file mode 100644 index 000000000..ac46d8b81 --- /dev/null +++ b/src/SmartHopper.Providers.LocalAI/SmartHopper.Providers.LocalAI.csproj @@ -0,0 +1,78 @@ + + + + + net7.0-windows;net7.0 + true + NU1701;NETSDK1086;SA1124;SA1200 + true + true + + + + SmartHopper LocalAI Provider + LocalAI provider for SmartHopper. Connects to a self-hosted LocalAI instance via its OpenAI-compatible API. + + + + + $(SolutionDir)bin/$(SolutionVersion)/$(Configuration) + + + + + + + + + true + $(DefineConstants);WINDOWS + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/SmartHopper.Providers.MistralAI/MistralAIJsonSchemaAdapter.cs b/src/SmartHopper.Providers.MistralAI/MistralAIJsonSchemaAdapter.cs index 96cecd83f..5146223f0 100644 --- a/src/SmartHopper.Providers.MistralAI/MistralAIJsonSchemaAdapter.cs +++ b/src/SmartHopper.Providers.MistralAI/MistralAIJsonSchemaAdapter.cs @@ -18,7 +18,7 @@ using System; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; namespace SmartHopper.Providers.MistralAI { diff --git a/src/SmartHopper.Providers.MistralAI/MistralAIProvider.cs b/src/SmartHopper.Providers.MistralAI/MistralAIProvider.cs index 3ff433153..f818eb2b5 100644 --- a/src/SmartHopper.Providers.MistralAI/MistralAIProvider.cs +++ b/src/SmartHopper.Providers.MistralAI/MistralAIProvider.cs @@ -27,19 +27,19 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.JsonSchemas; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Streaming; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Streaming; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Providers.MistralAI { @@ -399,7 +399,7 @@ public override string Encode(AIRequestCall request) } // Only send for models that support reasoning, or when the user explicitly requests it per-request. - var supportsReasoning = ModelManager.Instance?.GetCapabilities(this.Name, request.Model)?.HasCapability(AICapability.Reasoning) == true; + var supportsReasoning = AIModelCapabilityRegistry.Instance?.GetCapabilities(this.Name, request.Model)?.HasCapability(AICapability.Reasoning) == true; if (hasExplicitReasoningEffort || supportsReasoning) { requestBody["reasoning_effort"] = reasoningEffort; diff --git a/src/SmartHopper.Providers.MistralAI/MistralAIProviderFactory.cs b/src/SmartHopper.Providers.MistralAI/MistralAIProviderFactory.cs index 1a038fabf..9bd0f5e1a 100644 --- a/src/SmartHopper.Providers.MistralAI/MistralAIProviderFactory.cs +++ b/src/SmartHopper.Providers.MistralAI/MistralAIProviderFactory.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.MistralAI { diff --git a/src/SmartHopper.Providers.MistralAI/MistralAIProviderModels.cs b/src/SmartHopper.Providers.MistralAI/MistralAIProviderModels.cs index 13a5c765c..d59f48202 100644 --- a/src/SmartHopper.Providers.MistralAI/MistralAIProviderModels.cs +++ b/src/SmartHopper.Providers.MistralAI/MistralAIProviderModels.cs @@ -22,10 +22,10 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.MistralAI { @@ -212,6 +212,17 @@ public override Task> RetrieveModels() Aliases = new List { "codestral-embed-2505" }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "glm-5-2", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + Verified = false, + Rank = 9960, + Aliases = new List { "zai-glm-5-2" }, + }, + new AIModelCapabilities { Provider = provider, @@ -219,7 +230,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.ImageInput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, Verified = false, - Rank = 9960, + Rank = 9955, Aliases = new List { "labs-leanstral-1-5" }, }, @@ -230,7 +241,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.EmbedOutput, SupportsStreaming = false, Verified = false, - Rank = 9955, + Rank = 9950, Aliases = new List { "mistral-embed" }, }, @@ -241,11 +252,21 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.ImageInput | AICapability.ImageOutput, SupportsStreaming = false, Verified = false, - Rank = 9950, + Rank = 9945, Aliases = new List { "mistral-ocr-3-0", "mistral-ocr-3" }, DiscouragedForTools = new List { "*" }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "mistral-ocr-4-0", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.ImageInput | AICapability.ImageOutput, + SupportsStreaming = true, + Verified = false, + Rank = 9940, + }, + new AIModelCapabilities { Provider = provider, @@ -253,8 +274,8 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.ImageInput | AICapability.ImageOutput, SupportsStreaming = true, Verified = false, - Rank = 9945, - Aliases = new List { "mistral-ocr-4-0", "mistral-ocr-4" }, + Rank = 9935, + Aliases = new List { "mistral-ocr-4", "mistral-ocr-4-1" }, }, new AIModelCapabilities @@ -265,7 +286,7 @@ public override Task> RetrieveModels() Default = AICapability.Speech2Text, SupportsStreaming = false, Verified = false, - Rank = 9940, + Rank = 9930, ContextLimit = 32000, Aliases = new List { "voxtral-mini-transcribe-2602", "voxtral-mini-latest" }, }, @@ -278,7 +299,7 @@ public override Task> RetrieveModels() Default = AICapability.Text2Speech, SupportsStreaming = false, Verified = false, - Rank = 9935, + Rank = 9925, Aliases = new List { "voxtral-mini-tts-latest" }, }, @@ -289,7 +310,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.AudioInput | AICapability.TextOutput, SupportsStreaming = false, Verified = false, - Rank = 9930, + Rank = 9920, ContextLimit = 32000, Aliases = new List { "voxtral-small-latest" }, }, diff --git a/src/SmartHopper.Providers.MistralAI/MistralAIProviderSettings.cs b/src/SmartHopper.Providers.MistralAI/MistralAIProviderSettings.cs index 09d5dcc1f..1468de1bc 100644 --- a/src/SmartHopper.Providers.MistralAI/MistralAIProviderSettings.cs +++ b/src/SmartHopper.Providers.MistralAI/MistralAIProviderSettings.cs @@ -20,9 +20,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Dialogs; -using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Providers.MistralAI { @@ -158,7 +159,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Max tokens must be a positive number.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Max tokens must be a positive number.")); } return false; @@ -178,9 +179,13 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError( - "Temperature for MistralAI models must be between 0.0 and 3.0.", - "Validation Error"); + ProviderSdkHost.Diagnostics.Report( + this.GetType().Name, + new SHRuntimeMessage( + SHRuntimeMessageSeverity.Error, + SHRuntimeMessageOrigin.Validation, + SHMessageCode.InputInvalid, + "Temperature for MistralAI models must be between 0.0 and 3.0.")); } return false; @@ -195,7 +200,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Reasoning effort for MistralAI must be 'none' or 'high'.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Reasoning effort for MistralAI must be 'none' or 'high'.")); } return false; diff --git a/src/SmartHopper.Providers.MistralAI/SmartHopper.Providers.MistralAI.csproj b/src/SmartHopper.Providers.MistralAI/SmartHopper.Providers.MistralAI.csproj index 96ea2df7a..b50cd0c79 100644 --- a/src/SmartHopper.Providers.MistralAI/SmartHopper.Providers.MistralAI.csproj +++ b/src/SmartHopper.Providers.MistralAI/SmartHopper.Providers.MistralAI.csproj @@ -40,7 +40,7 @@ - + diff --git a/src/SmartHopper.Providers.Ollama/OllamaJsonSchemaAdapter.cs b/src/SmartHopper.Providers.Ollama/OllamaJsonSchemaAdapter.cs new file mode 100644 index 000000000..3ef26cce2 --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/OllamaJsonSchemaAdapter.cs @@ -0,0 +1,91 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using Newtonsoft.Json.Linq; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; + +namespace SmartHopper.Providers.Ollama +{ + /// + /// JSON schema adapter for Ollama. + /// Ollama's OpenAI-compatible endpoint accepts a JSON object response_format; + /// non-object root schemas are wrapped into an object to remain compatible. + /// + internal sealed class OllamaJsonSchemaAdapter : IJsonSchemaAdapter + { + /// + public string ProviderName => OllamaProvider.NameValue; + + /// + public (JObject wrapped, SchemaWrapperInfo info) Wrap(JObject schema) + { + if (schema is null) + { + throw new ArgumentNullException(nameof(schema)); + } + + var schemaType = schema["type"]?.ToString(); + + if (string.Equals(schemaType, "object", StringComparison.OrdinalIgnoreCase)) + { + return (schema, new SchemaWrapperInfo { IsWrapped = false, ProviderName = this.ProviderName }); + } + + if (string.Equals(schemaType, "array", StringComparison.OrdinalIgnoreCase)) + { + var wrapped = new JObject + { + ["type"] = "object", + ["properties"] = new JObject { ["items"] = schema }, + ["required"] = new JArray { "items" }, + ["additionalProperties"] = false, + }; + return (wrapped, new SchemaWrapperInfo { IsWrapped = true, WrapperType = "array", PropertyName = "items", ProviderName = this.ProviderName }); + } + + if (schemaType == "string" || schemaType == "number" || schemaType == "integer" || schemaType == "boolean") + { + var wrapped = new JObject + { + ["type"] = "object", + ["properties"] = new JObject { ["value"] = schema }, + ["required"] = new JArray { "value" }, + ["additionalProperties"] = false, + }; + return (wrapped, new SchemaWrapperInfo { IsWrapped = true, WrapperType = schemaType ?? "primitive", PropertyName = "value", ProviderName = this.ProviderName }); + } + + var generic = new JObject + { + ["type"] = "object", + ["properties"] = new JObject { ["data"] = schema }, + ["required"] = new JArray { "data" }, + ["additionalProperties"] = false, + }; + return (generic, new SchemaWrapperInfo { IsWrapped = true, WrapperType = "unknown", PropertyName = "data", ProviderName = this.ProviderName }); + } + + /// + public string Unwrap(string content, SchemaWrapperInfo info) + { + // Default service logic handles unwrap for object-root payloads + return content; + } + } +} diff --git a/src/SmartHopper.Providers.Ollama/OllamaProvider.Streaming.cs b/src/SmartHopper.Providers.Ollama/OllamaProvider.Streaming.cs new file mode 100644 index 000000000..7fe1b5aad --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/OllamaProvider.Streaming.cs @@ -0,0 +1,405 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Streaming; + +namespace SmartHopper.Providers.Ollama +{ + /// + /// Streaming half of . + /// + public sealed partial class OllamaProvider + { + /// + protected override IStreamingAdapter CreateStreamingAdapter() + { + return new OllamaStreamingAdapter(this); + } + + /// + /// Provider-scoped streaming adapter for Ollama Chat Completions SSE. + /// Ollama follows the OpenAI streaming protocol (data: {...}\n\n + [DONE]). + /// + private sealed class OllamaStreamingAdapter : AIProviderStreamingAdapter, IStreamingAdapter + { + public OllamaStreamingAdapter(OllamaProvider provider) + : base(provider) + { + } + + public async IAsyncEnumerable StreamAsync( + AIRequestCall request, + StreamingOptions options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (request == null) + { + yield break; + } + + request = this.Prepare(request); + + if (!string.Equals(request.Endpoint, "/chat/completions", StringComparison.Ordinal)) + { + var unsupported = new AIReturn(); + unsupported.CreateProviderError("Streaming is only supported for /chat/completions in this adapter.", request); + yield return unsupported; + yield break; + } + + JObject body; + try + { + body = JObject.Parse(this.Provider.Encode(request)); + } + catch + { + body = new JObject(); + } + + body["stream"] = true; + + var fullUrl = this.BuildFullUrl(request.Endpoint); + + using var httpClient = this.CreateHttpClient(); + AIReturn? authError = null; + try + { + var apiKey = ((OllamaProvider)this.Provider).GetApiKey(); + this.ApplyAuthentication(httpClient, request.Authentication, apiKey); + } + catch (Exception ex) + { + authError = new AIReturn(); + authError.CreateProviderError(ex.Message, request); + } + + if (authError != null) + { + yield return authError; + yield break; + } + + using var httpRequest = this.CreateSsePost(fullUrl, body.ToString(), "application/json"); + + HttpResponseMessage response; + AIReturn? sendError = null; + try + { + response = await this.SendForStreamAsync(httpClient, httpRequest, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + sendError = new AIReturn(); + sendError.CreateNetworkError(ex.InnerException?.Message ?? ex.Message, request); + response = null!; + } + + if (sendError != null) + { + yield return sendError; + yield break; + } + + if (!response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + var (message, isNetworkLike) = AIProvider.ClassifyHttpError((int)response.StatusCode, response.ReasonPhrase, content, this.Provider.Name); + var err = new AIReturn(); + if (isNetworkLike) + { + err.CreateNetworkError(message, request); + } + else + { + err.CreateProviderError(message, request); + } + + yield return err; + yield break; + } + + var buffer = new StringBuilder(); + var lastEmit = DateTime.UtcNow; + var firstChunk = true; + string finalFinishReason = string.Empty; + int promptTokens = 0; + int completionTokens = 0; + + var assistantAggregate = new AIInteractionText + { + Agent = AIAgent.Assistant, + Content = string.Empty, + Metrics = new AIMetrics { Provider = this.Provider.Name, Model = request.Model }, + }; + + // Tool call accumulation (index -> partial) + var toolCalls = new Dictionary(); + + async IAsyncEnumerable EmitAsync(string text, bool streamingStatus) + { + if (string.IsNullOrEmpty(text)) yield break; + + assistantAggregate.AppendDelta(contentDelta: text); + + var snapshot = new AIInteractionText + { + Agent = assistantAggregate.Agent, + Content = assistantAggregate.Content, + Metrics = new AIMetrics + { + Provider = assistantAggregate.Metrics.Provider, + Model = assistantAggregate.Metrics.Model, + FinishReason = assistantAggregate.Metrics.FinishReason, + InputTokensCached = assistantAggregate.Metrics.InputTokensCached, + InputTokensPrompt = assistantAggregate.Metrics.InputTokensPrompt, + OutputTokensReasoning = assistantAggregate.Metrics.OutputTokensReasoning, + OutputTokensGeneration = assistantAggregate.Metrics.OutputTokensGeneration, + CompletionTime = assistantAggregate.Metrics.CompletionTime, + }, + }; + + var delta = new AIReturn + { + Request = request, + Status = streamingStatus ? AICallStatus.Streaming : AICallStatus.Processing, + }; + delta.SetBody(new List { snapshot }); + yield return delta; + await Task.Yield(); + } + + async Task> FlushAsync(bool force) + { + var results = new List(); + if (buffer.Length == 0) return results; + var elapsed = (DateTime.UtcNow - lastEmit).TotalMilliseconds; + if (force || !options.CoalesceTokens || buffer.Length >= options.PreferredChunkSize || elapsed >= options.CoalesceDelayMs) + { + var text = buffer.ToString(); + buffer.Clear(); + lastEmit = DateTime.UtcNow; + await foreach (var d in EmitAsync(text, streamingStatus: true).WithCancellation(cancellationToken)) + { + results.Add(d); + } + } + + return results; + } + + { + var initial = new AIReturn { Request = request, Status = AICallStatus.Processing }; + initial.SetBody(new List()); + yield return initial; + } + + var idleTimeout = TimeSpan.FromSeconds((double)(request.TimeoutSeconds > 0 ? request.TimeoutSeconds : TimeoutDefaults.DefaultTimeoutSeconds)); + await foreach (var data in this.ReadSseDataAsync( + response, + idleTimeout, + null, + cancellationToken).WithCancellation(cancellationToken)) + { + JObject parsed; + try + { + parsed = JObject.Parse(data); + } + catch + { + continue; + } + + var choices = parsed["choices"] as JArray; + var choice = choices?.FirstOrDefault() as JObject; + var delta = choice?["delta"] as JObject; + var finishReason = choice?["finish_reason"]?.ToString(); + bool hasFinish = !string.IsNullOrEmpty(finishReason); + if (hasFinish) finalFinishReason = finishReason; + + var usage = parsed["usage"] as JObject; + if (usage != null) + { + var pt = usage["prompt_tokens"]?.Value(); + var ct = usage["completion_tokens"]?.Value(); + if (pt.HasValue) promptTokens = pt.Value; + if (ct.HasValue) completionTokens = ct.Value; + + assistantAggregate.AppendDelta(metricsDelta: new AIMetrics + { + Provider = this.Provider.Name, + Model = request.Model, + InputTokensPrompt = pt ?? 0, + OutputTokensGeneration = ct ?? 0, + }); + } + + var contentDelta = delta?["content"]?.ToString(); + if (!string.IsNullOrEmpty(contentDelta)) + { + buffer.Append(contentDelta); + var emitted = await FlushAsync(force: firstChunk).ConfigureAwait(false); + if (firstChunk) firstChunk = false; + foreach (var d in emitted) + { + yield return d; + } + } + + var tcArray = delta?["tool_calls"] as JArray; + if (tcArray != null) + { + foreach (var t in tcArray.OfType()) + { + var idx = t["index"]?.Value() ?? 0; + if (!toolCalls.TryGetValue(idx, out var entry)) + { + entry = (Id: string.Empty, Name: string.Empty, Args: new StringBuilder()); + toolCalls[idx] = entry; + } + + var idVal = t["id"]?.ToString(); + if (!string.IsNullOrEmpty(idVal)) entry.Id = idVal; + + var func = t["function"] as JObject; + if (func != null) + { + var name = func["name"]?.ToString(); + if (!string.IsNullOrEmpty(name)) entry.Name = name; + var args = func["arguments"]?.ToString(); + if (!string.IsNullOrEmpty(args)) entry.Args.Append(args); + } + + toolCalls[idx] = entry; + } + + var emittedTc = await FlushAsync(force: true).ConfigureAwait(false); + foreach (var d in emittedTc) + { + yield return d; + } + + var interactions = new List(); + foreach (var kv in toolCalls.OrderBy(k => k.Key)) + { + var (id, name, argsSb) = kv.Value; + JObject? argsObj = null; + var argsStr = argsSb.ToString(); + try + { + if (!string.IsNullOrWhiteSpace(argsStr)) + { + argsObj = JObject.Parse(argsStr); + } + } + catch + { + // Partial JSON, ignore + } + + interactions.Add(new AIInteractionToolCall { Id = id, Name = name, Arguments = argsObj }); + } + + var tcDelta = new AIReturn { Request = request, Status = AICallStatus.CallingTools }; + tcDelta.SetBody(interactions); + yield return tcDelta; + } + } + + var finalEmitted = await FlushAsync(force: true).ConfigureAwait(false); + foreach (var d in finalEmitted) + { + yield return d; + } + + var final = new AIReturn + { + Request = request, + Status = AICallStatus.Finished, + }; + + assistantAggregate.AppendDelta(metricsDelta: new AIMetrics + { + FinishReason = string.IsNullOrEmpty(finalFinishReason) ? "stop" : finalFinishReason, + }); + + var finalBuilder = AIBodyBuilder.Create(); + if (!string.IsNullOrEmpty(assistantAggregate.Content)) + { + var finalSnapshot = new AIInteractionText + { + Agent = assistantAggregate.Agent, + Content = assistantAggregate.Content, + Metrics = new AIMetrics + { + Provider = assistantAggregate.Metrics.Provider, + Model = assistantAggregate.Metrics.Model, + FinishReason = assistantAggregate.Metrics.FinishReason, + InputTokensCached = assistantAggregate.Metrics.InputTokensCached, + InputTokensPrompt = assistantAggregate.Metrics.InputTokensPrompt, + OutputTokensReasoning = assistantAggregate.Metrics.OutputTokensReasoning, + OutputTokensGeneration = assistantAggregate.Metrics.OutputTokensGeneration, + CompletionTime = assistantAggregate.Metrics.CompletionTime, + }, + }; + finalBuilder.Add(finalSnapshot, markAsNew: false); + } + + foreach (var kv in toolCalls.OrderBy(k => k.Key)) + { + var (id, name, argsSb) = kv.Value; + JObject? argsObj = null; + var argsStr = argsSb.ToString(); + try + { + if (!string.IsNullOrWhiteSpace(argsStr)) + { + argsObj = JObject.Parse(argsStr); + } + } + catch + { + // Partial JSON + } + + finalBuilder.Add(new AIInteractionToolCall { Id = id, Name = name, Arguments = argsObj }, markAsNew: false); + } + + final.SetBody(finalBuilder.Build()); + yield return final; + } + } + } +} diff --git a/src/SmartHopper.Providers.Ollama/OllamaProvider.cs b/src/SmartHopper.Providers.Ollama/OllamaProvider.cs new file mode 100644 index 000000000..fed0c8480 --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/OllamaProvider.cs @@ -0,0 +1,586 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Streaming; + +namespace SmartHopper.Providers.Ollama +{ + /// + /// Ollama provider implementation. + /// Targets local Ollama instances (https://ollama.com/) through the + /// OpenAI-compatible /v1/chat/completions endpoint. + /// + public sealed partial class OllamaProvider : AIProvider + { + /// + /// The name of the provider. Used by the UI for provider selection. + /// + public static readonly string NameValue = "Ollama"; + + /// + /// Fallback default server URL when no Base URL has been configured. + /// Ollama defaults to port 11434 on localhost. + /// + public static readonly Uri FallbackServerUrl = new ("http://localhost:11434/v1"); + + /// + /// Initializes a new instance of the class. + /// Private constructor to enforce singleton pattern. + /// + private OllamaProvider() + { + this.Models = new OllamaProviderModels(this); + + // Register provider-specific JSON schema adapter (object-root wrapper, OpenAI-style) + JsonSchemaAdapterRegistry.Register(new OllamaJsonSchemaAdapter()); + } + + /// + public override string Name => NameValue; + + /// + /// Gets the server URL configured for this provider. + /// Reads the user-provided ServerUrl setting and falls back to + /// when the setting is empty or not a valid absolute HTTP(S) URL. + /// + public override Uri DefaultServerUrl + { + get + { + var configured = this.GetServerUrlSetting(); + if (!string.IsNullOrWhiteSpace(configured) + && Uri.TryCreate(configured, UriKind.Absolute, out var uri) + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + { + return uri; + } + + return FallbackServerUrl; + } + } + + /// + public override bool IsEnabled => true; + + /// + /// Ollama is configured when a Base URL is available. + /// The descriptor supplies a default loopback URL, so the provider is considered + /// configured out of the box unless the URL has been explicitly cleared. + /// + public override bool IsConfigured => !string.IsNullOrWhiteSpace(this.GetServerUrlSetting()); + + /// + /// Helper to retrieve the configured API key for this provider. + /// Exposed to the nested streaming adapter to avoid protected access issues. + /// + internal string GetApiKey() + { + return this.GetSetting("ApiKey"); + } + + /// + /// Helper to retrieve the configured Base URL setting (raw string). + /// + internal string GetServerUrlSetting() + { + return this.GetSetting("ServerUrl"); + } + + /// + public override Image Icon + { + get + { + var iconBytes = Properties.Resources.ollama_icon; + using (var ms = new MemoryStream(iconBytes)) + { + return new Bitmap(ms); + } + } + } + + /// + public override AIRequestCall PreCall(AIRequestCall request) + { + // First do the base PreCall + request = base.PreCall(request); + + // Setup HTTP method, content type, and authentication. + request.HttpMethod = "POST"; + request.ContentType = "application/json"; + + // Ollama does not require an API key by default; the OpenAI-compatible + // endpoint accepts an arbitrary placeholder. Use bearer auth only when + // the user has explicitly configured one (e.g., a reverse-proxy in front). + var apiKey = this.GetApiKey(); + request.Authentication = string.IsNullOrWhiteSpace(apiKey) ? "none" : "bearer"; + + // Ollama exposes the OpenAI-compatible chat completions endpoint + request.Endpoint = "/chat/completions"; + + return request; + } + + /// + public override string Encode(AIRequestCall request) + { + if (request.HttpMethod == "GET" || request.HttpMethod == "DELETE") + { + return "GET and DELETE requests do not use a request body"; + } + + var p = request.Parameters; + + int maxTokens = p?.MaxTokens ?? this.GetSetting("MaxTokens"); + double temperature = p?.Temperature ?? this.GetSetting("Temperature"); + string? toolFilter = request.Body.ToolFilter; + + Debug.WriteLine($"[Ollama] Encode - Model: {request.Model}, MaxTokens: {maxTokens}"); + + // Simple sequential encoding (same approach as OpenAI/MistralAI/DeepSeek providers). + var convertedMessages = new JArray(); + + // Merge System and Summary interactions before encoding + var mergedInteractions = this.MergeSystemAndSummary(request.Body.Interactions); + + foreach (var interaction in mergedInteractions) + { + try + { + var token = this.EncodeToJToken(interaction); + if (token == null) + { + continue; + } + + var role = token["role"]?.ToString(); + if (string.IsNullOrEmpty(role)) + { + continue; + } + + convertedMessages.Add(token); + } + catch (Exception ex) + { + Debug.WriteLine($"[Ollama] Warning: Could not encode interaction: {ex.Message}"); + } + } + + // Build request body + var requestBody = new JObject + { + ["model"] = request.Model, + ["messages"] = convertedMessages, + ["max_tokens"] = maxTokens, + ["temperature"] = temperature, + }; + + // Apply optional parameters from extras only + if (p?.Extras != null) + { + if (p.Extras.TryGetValue("top_p", out var topPToken) && topPToken != null) + { + requestBody["top_p"] = topPToken.Value(); + } + + if (p.Extras.TryGetValue("top_k", out var topKToken) && topKToken != null) + { + requestBody["top_k"] = topKToken.Value(); + } + + if (p.Extras.TryGetValue("seed", out var seedToken) && seedToken != null) + { + requestBody["seed"] = seedToken.Value(); + } + + if (p.Extras.TryGetValue("presence_penalty", out var ppToken) && ppToken != null) + { + requestBody["presence_penalty"] = ppToken; + } + + if (p.Extras.TryGetValue("frequency_penalty", out var fpToken) && fpToken != null) + { + requestBody["frequency_penalty"] = fpToken; + } + } + + // Add JSON response format if schema is provided (centralized wrapping) + if (!string.IsNullOrWhiteSpace(request.Body.JsonOutputSchema)) + { + try + { + var svc = JsonSchemaService.Instance; + var schemaObj = JObject.Parse(request.Body.JsonOutputSchema); + var (wrappedSchema, wrapperInfo) = svc.WrapForProvider(schemaObj, this.Name); + svc.SetCurrentWrapperInfo(wrapperInfo); + Debug.WriteLine($"[Ollama] Schema wrapper info stored (central): IsWrapped={wrapperInfo.IsWrapped}, Type={wrapperInfo.WrapperType}, Property={wrapperInfo.PropertyName}"); + + // Ollama supports json_object response_format on its OpenAI-compatible endpoint + requestBody["response_format"] = new JObject { ["type"] = "json_object" }; + + // Add a system guidance message including the wrapped schema + var systemMessage = new JObject + { + ["role"] = "system", + ["content"] = "The response must be a valid JSON object that strictly follows this schema: " + wrappedSchema.ToString(Newtonsoft.Json.Formatting.None), + }; + convertedMessages.Insert(0, systemMessage); + } + catch (Exception ex) + { + Debug.WriteLine($"[Ollama] Failed to parse/handle JSON schema: {ex.Message}"); + JsonSchemaService.Instance.SetCurrentWrapperInfo(new SchemaWrapperInfo { IsWrapped = false }); + requestBody["response_format"] = new JObject { ["type"] = "text" }; + } + } + else + { + JsonSchemaService.Instance.SetCurrentWrapperInfo(new SchemaWrapperInfo { IsWrapped = false }); + requestBody["response_format"] = new JObject { ["type"] = "text" }; + } + + // Add tools if requested + if (!string.IsNullOrWhiteSpace(toolFilter)) + { + var tools = this.GetFormattedTools(toolFilter); + if (tools != null && tools.Count > 0) + { + requestBody["tools"] = tools; + + // Forced tool call uses OpenAI-compatible tool_choice with function name + if (request.ForceToolCall && !string.IsNullOrWhiteSpace(request.ForceToolName)) + { + requestBody["tool_choice"] = new JObject + { + ["type"] = "function", + ["function"] = new JObject { ["name"] = request.ForceToolName, }, + }; + Debug.WriteLine($"[Ollama] Forcing tool call: {request.ForceToolName}"); + } + else + { + requestBody["tool_choice"] = "auto"; + } + } + } + + return requestBody.ToString(); + } + + /// + public override string Encode(IAIInteraction interaction) + { + try + { + var token = this.EncodeToJToken(interaction); + return token?.ToString() ?? string.Empty; + } + catch (Exception ex) + { + Debug.WriteLine($"[Ollama] Encode(IAIInteraction) error: {ex.Message}"); + return string.Empty; + } + } + + /// + /// Converts a single interaction into a Ollama/OpenAI-compatible message object. + /// Returns null for interactions that should not be sent (e.g., UI-only diagnostics). + /// + private JToken? EncodeToJToken(IAIInteraction interaction) + { + if (interaction == null) + { + return null; + } + + // UI-only diagnostics must not be sent to providers + if (interaction is AIInteractionRuntimeMessage) + { + return null; + } + + // Map role + string role; + switch (interaction.Agent) + { + case AIAgent.System: + case AIAgent.Context: + role = "system"; + break; + case AIAgent.User: + role = "user"; + break; + case AIAgent.Assistant: + case AIAgent.ToolCall: + role = "assistant"; + break; + case AIAgent.ToolResult: + role = "tool"; + break; + default: + return null; + } + + var messageObj = new JObject { ["role"] = role }; + + if (interaction is AIInteractionText textInteraction) + { + messageObj["content"] = textInteraction.Content ?? string.Empty; + } + else if (interaction is AIInteractionToolResult toolResultInteraction) + { + messageObj["content"] = toolResultInteraction.Result != null + ? JsonConvert.SerializeObject(toolResultInteraction.Result, Formatting.None) + : string.Empty; + + if (!string.IsNullOrWhiteSpace(toolResultInteraction.Id)) + { + messageObj["tool_call_id"] = toolResultInteraction.Id; + } + + if (!string.IsNullOrWhiteSpace(toolResultInteraction.Name)) + { + messageObj["name"] = toolResultInteraction.Name; + } + } + else if (interaction is AIInteractionToolCall toolCallInteraction) + { + var toolCallObj = new JObject + { + ["id"] = toolCallInteraction.Id ?? string.Empty, + ["type"] = "function", + ["function"] = new JObject + { + ["name"] = toolCallInteraction.Name ?? string.Empty, + ["arguments"] = toolCallInteraction.Arguments?.ToString() ?? "{}", + }, + }; + messageObj["tool_calls"] = new JArray { toolCallObj }; + messageObj["content"] = string.Empty; + } + else if (interaction is AIInteractionImage imageInteraction) + { + // Ollama vision support depends on the loaded model; fall back to the original prompt + messageObj["content"] = imageInteraction.OriginalPrompt ?? string.Empty; + } + else + { + messageObj["content"] = string.Empty; + } + + return messageObj; + } + + /// + public override List Decode(JObject response) + { + var interactions = new List(); + if (response == null) + { + return interactions; + } + + try + { + // OpenAI-compatible error envelope: {"error": {"message": "...", "type": "..."}} + if (response["error"] is JObject errorObj) + { + var errMsg = errorObj["message"]?.ToString() + ?? errorObj["type"]?.ToString() + ?? "Provider returned an error"; + Debug.WriteLine($"[Ollama] Decode: provider error in response body: {errMsg}"); + interactions.Add(new AIInteractionRuntimeMessage { Severity = SHRuntimeMessageSeverity.Error, Content = errMsg }); + return interactions; + } + + var choices = response["choices"] as JArray; + var firstChoice = choices?.FirstOrDefault() as JObject; + var message = firstChoice?["message"] as JObject; + if (message == null) + { + Debug.WriteLine("[Ollama] Decode: No message found in response"); + return interactions; + } + + // Robust content extraction: handle string, array of parts, or object content + string content; + var contentToken = message["content"]; + if (contentToken is JArray contentParts && contentParts.Count > 0) + { + var texts = new List(); + foreach (var part in contentParts) + { + var text = part?["text"]?.ToString() + ?? part?["content"]?.ToString() + ?? part?.ToString(Newtonsoft.Json.Formatting.None) + ?? string.Empty; + if (!string.IsNullOrWhiteSpace(text)) + { + texts.Add(text); + } + } + + content = string.Join("\n", texts); + } + else + { + content = contentToken?.ToString() ?? string.Empty; + } + + // Apply centralized unwrapping using stored wrapper info + var wrapperInfo = JsonSchemaService.Instance.GetCurrentWrapperInfo(); + if (wrapperInfo != null && wrapperInfo.IsWrapped) + { + Debug.WriteLine($"[Ollama] Unwrapping response content using wrapper info (central): Type={wrapperInfo.WrapperType}, Property={wrapperInfo.PropertyName}"); + content = JsonSchemaService.Instance.Unwrap(content, wrapperInfo); + } + + // Some Ollama builds surface reasoning text via reasoning_content (e.g. when running thinking models) + var reasoning = message["reasoning_content"]?.ToString(); + + var tcs = message["tool_calls"] as JArray; + + // Avoid emitting an empty assistant text interaction for pure tool-call responses + if (!string.IsNullOrEmpty(content) + || !string.IsNullOrEmpty(reasoning) + || (tcs != null && tcs.Count > 0)) + { + var interaction = new AIInteractionText(); + interaction.SetResult( + agent: AIAgent.Assistant, + content: content, + reasoning: string.IsNullOrWhiteSpace(reasoning) ? null : reasoning); + interaction.Metrics = this.DecodeMetrics(response); + interactions.Add(interaction); + } + + // Add an AIInteractionToolCall for each tool call + if (tcs != null && tcs.Count > 0) + { + foreach (JObject tc in tcs.OfType()) + { + var function = tc["function"] as JObject; + var argumentsStr = function?["arguments"]?.ToString() ?? "{}"; + JObject? argumentsObj = null; + try + { + if (!string.IsNullOrWhiteSpace(argumentsStr)) + { + argumentsObj = JObject.Parse(argumentsStr); + } + } + catch + { + // Some local backends emit non-JSON arguments; pass them through as-is + } + + var toolCall = new AIInteractionToolCall + { + Id = tc["id"]?.ToString(), + Name = function?["name"]?.ToString(), + Arguments = argumentsObj, + Reasoning = string.IsNullOrWhiteSpace(reasoning) ? null : reasoning, + }; + interactions.Add(toolCall); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"[Ollama] Decode error: {ex.Message}"); + } + + return interactions; + } + + /// + /// Decodes the metrics from the response (token usage + finish reason). + /// + private AIMetrics DecodeMetrics(JObject response) + { + var choices = response["choices"] as JArray; + var firstChoice = choices?.FirstOrDefault() as JObject; + var usage = response["usage"] as JObject; + + var metrics = new AIMetrics + { + Provider = this.Name, + FinishReason = firstChoice?["finish_reason"]?.ToString() ?? string.Empty, + InputTokensPrompt = usage?["prompt_tokens"]?.Value() ?? 0, + OutputTokensGeneration = usage?["completion_tokens"]?.Value() ?? 0, + }; + + return metrics; + } + + /// + public override IEnumerable GetExtraDescriptors() + { + return new[] + { + new AIExtraDescriptor( + "top_p", + "Top P", + "Nucleus sampling parameter (0.0-1.0). Lower values make output more focused; higher values more diverse. Leave empty to use the model default.", + typeof(double), + null), + new AIExtraDescriptor( + "top_k", + "Top K", + "Limits the next-token candidate pool to the K most likely tokens. Leave empty to use the model default.", + typeof(int), + null), + new AIExtraDescriptor( + "seed", + "Seed", + "Reproducibility seed for deterministic sampling. Use the same seed to get similar outputs. Leave empty for random.", + typeof(int), + null), + new AIExtraDescriptor( + "presence_penalty", + "Presence Penalty", + "Penalizes tokens already present in the text (-2.0 to 2.0). Positive values encourage new topics.", + typeof(double), + null), + new AIExtraDescriptor( + "frequency_penalty", + "Frequency Penalty", + "Penalizes frequent tokens (-2.0 to 2.0). Positive values reduce repetition.", + typeof(double), + null), + }; + } + } +} diff --git a/src/SmartHopper.Providers.Ollama/OllamaProviderFactory.cs b/src/SmartHopper.Providers.Ollama/OllamaProviderFactory.cs new file mode 100644 index 000000000..11980e97a --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/OllamaProviderFactory.cs @@ -0,0 +1,41 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using SmartHopper.ProviderSdk.AIProviders; + +namespace SmartHopper.Providers.Ollama +{ + /// + /// Factory class for creating instances of the Ollama provider and its settings. + /// This class is discovered by the ProviderManager through reflection. + /// + public class OllamaProviderFactory : IAIProviderFactory + { + /// + public IAIProvider CreateProvider() + { + return OllamaProvider.Instance; + } + + /// + public IAIProviderSettings CreateProviderSettings() + { + return new OllamaProviderSettings(OllamaProvider.Instance); + } + } +} diff --git a/src/SmartHopper.Providers.Ollama/OllamaProviderModels.cs b/src/SmartHopper.Providers.Ollama/OllamaProviderModels.cs new file mode 100644 index 000000000..6eb18d0e1 --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/OllamaProviderModels.cs @@ -0,0 +1,54 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System.Collections.Generic; +using System.Threading.Tasks; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; + +namespace SmartHopper.Providers.Ollama +{ + /// + /// Ollama provider-specific model management implementation. + /// + /// + /// Ollama is run locally and the available models depend entirely on what the + /// user has pulled with ollama pull. We therefore do not ship a static catalog + /// of curated models. The user supplies the model name (e.g. llama3.1, + /// qwen2.5:14b) through the provider's Model setting; we return an empty list + /// and rely on the user-supplied name. + /// + public class OllamaProviderModels : AIProviderModels + { + /// + /// Initializes a new instance of the class. + /// + /// The Ollama provider instance. + public OllamaProviderModels(OllamaProvider provider) + : base(provider) + { + } + + /// + public override Task> RetrieveModels() + { + // Models are user-managed in Ollama (pulled with `ollama pull`); no static catalog. + return Task.FromResult(new List()); + } + } +} diff --git a/src/SmartHopper.Providers.Ollama/OllamaProviderSettings.cs b/src/SmartHopper.Providers.Ollama/OllamaProviderSettings.cs new file mode 100644 index 000000000..d9399e9d8 --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/OllamaProviderSettings.cs @@ -0,0 +1,174 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; + +namespace SmartHopper.Providers.Ollama +{ + /// + /// Settings implementation for the Ollama provider. + /// Exposes a configurable Base URL pointing at a local Ollama instance, + /// plus an optional API key for installations placed behind an authenticating proxy. + /// + public class OllamaProviderSettings : AIProviderSettings + { + private new readonly IAIProvider provider; + + /// + /// Initializes a new instance of the class. + /// + /// The provider associated with these settings. + public OllamaProviderSettings(IAIProvider provider) + : base(provider) + { + this.provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + /// + public override IEnumerable GetSettingDescriptors() + { + return new[] + { + new SettingDescriptor + { + Name = "ServerUrl", + DisplayName = "Base URL", + Description = "Base URL of your Ollama server, including the OpenAI-compatible API prefix (e.g. http://localhost:11434/v1).", + Type = typeof(string), + DefaultValue = OllamaProvider.FallbackServerUrl.ToString(), + }, + new SettingDescriptor + { + Name = "ApiKey", + DisplayName = "API Key", + Description = "Optional API key. Ollama itself does not require authentication, but you can set one if your instance is behind a reverse proxy that enforces it.", + IsSecret = true, + Type = typeof(string), + }, + new SettingDescriptor + { + Name = "Model", + DisplayName = "Model", + Description = "Model tag as pulled with `ollama pull` (e.g. 'llama3.1', 'qwen2.5:14b', 'mistral:7b-instruct').", + Type = typeof(string), + }.Apply(d => d.SetLazyDefault(() => this.provider.GetDefaultModel())), + new SettingDescriptor + { + Name = "EnableStreaming", + Type = typeof(bool), + DefaultValue = true, + DisplayName = "Enable Streaming", + Description = "Allow streaming responses for this provider. When enabled, you will receive the response as it is generated.", + }, + new SettingDescriptor + { + Name = "MaxTokens", + DisplayName = "Max Tokens", + Description = "Maximum number of tokens to generate.", + Type = typeof(int), + DefaultValue = 2000, + ControlParams = new NumericSettingDescriptorControl + { + UseSlider = false, + Min = 1, + Max = 32768, + Step = 1, + }, + }, + new SettingDescriptor + { + Name = "Temperature", + Type = typeof(string), + DefaultValue = "0.5", + DisplayName = "Temperature", + Description = "Controls randomness (0.0-2.0). Higher values produce more diverse output; lower values are more focused and deterministic.", + }, + }; + } + + /// + public override bool ValidateSettings(Dictionary settings) + { + Debug.WriteLine($"[Ollama] ValidateSettings called. Settings null? {settings == null}"); + + if (settings == null) + { + return false; + } + + const bool showErrorDialogs = true; + + // Validate Base URL when supplied: must parse as an absolute HTTP(S) URI. + if (settings.TryGetValue("ServerUrl", out var serverUrlObj) && serverUrlObj != null) + { + var serverUrl = serverUrlObj.ToString(); + if (!string.IsNullOrWhiteSpace(serverUrl)) + { + if (!Uri.TryCreate(serverUrl, UriKind.Absolute, out var parsed) + || (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps)) + { + if (showErrorDialogs) + { + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Base URL must be an absolute HTTP or HTTPS URL (for example http://localhost:11434/v1).")); + } + + return false; + } + } + } + + // MaxTokens must be a positive integer + if (settings.TryGetValue("MaxTokens", out var maxTokensObj) && maxTokensObj != null) + { + if (!int.TryParse(maxTokensObj.ToString(), out var parsedMaxTokens) || parsedMaxTokens <= 0) + { + if (showErrorDialogs) + { + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Max tokens must be a positive number.")); + } + + return false; + } + } + + // Temperature must be in [0.0, 2.0] + if (settings.TryGetValue("Temperature", out var temperatureObj) && temperatureObj != null) + { + if (!double.TryParse(temperatureObj.ToString(), out var parsedTemperature) + || parsedTemperature < 0.0 + || parsedTemperature > 2.0) + { + if (showErrorDialogs) + { + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Temperature must be between 0.0 and 2.0.")); + } + + return false; + } + } + + return true; + } + } +} diff --git a/src/SmartHopper.Providers.Ollama/Properties/Resources.Designer.cs b/src/SmartHopper.Providers.Ollama/Properties/Resources.Designer.cs new file mode 100644 index 000000000..80c12ad09 --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/Properties/Resources.Designer.cs @@ -0,0 +1,75 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SmartHopper.Providers.Ollama.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SmartHopper.Providers.Ollama.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] ollama_icon { + get { + object obj = ResourceManager.GetObject("ollama_icon", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/src/SmartHopper.Providers.Ollama/Properties/Resources.resx b/src/SmartHopper.Providers.Ollama/Properties/Resources.resx new file mode 100644 index 000000000..6110ebd91 --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\ollama_icon.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/src/SmartHopper.Providers.Ollama/Resources/ollama_icon.png b/src/SmartHopper.Providers.Ollama/Resources/ollama_icon.png new file mode 100644 index 000000000..5b0ea6c32 Binary files /dev/null and b/src/SmartHopper.Providers.Ollama/Resources/ollama_icon.png differ diff --git a/src/SmartHopper.Providers.Ollama/SmartHopper.Providers.Ollama.csproj b/src/SmartHopper.Providers.Ollama/SmartHopper.Providers.Ollama.csproj new file mode 100644 index 000000000..06e2ba2bc --- /dev/null +++ b/src/SmartHopper.Providers.Ollama/SmartHopper.Providers.Ollama.csproj @@ -0,0 +1,78 @@ + + + + + net7.0-windows;net7.0 + true + NU1701;NETSDK1086;SA1124;SA1200 + true + true + + + + SmartHopper Ollama Provider + Ollama provider for SmartHopper. Connects to a local Ollama instance via its OpenAI-compatible API. + + + + + $(SolutionDir)bin/$(SolutionVersion)/$(Configuration) + + + + + + + + + true + $(DefineConstants);WINDOWS + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/SmartHopper.Providers.OpenAI/OpenAIJsonSchemaAdapter.cs b/src/SmartHopper.Providers.OpenAI/OpenAIJsonSchemaAdapter.cs index fde8e5853..07782c88a 100644 --- a/src/SmartHopper.Providers.OpenAI/OpenAIJsonSchemaAdapter.cs +++ b/src/SmartHopper.Providers.OpenAI/OpenAIJsonSchemaAdapter.cs @@ -18,7 +18,7 @@ using System; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; namespace SmartHopper.Providers.OpenAI { diff --git a/src/SmartHopper.Providers.OpenAI/OpenAIProvider.cs b/src/SmartHopper.Providers.OpenAI/OpenAIProvider.cs index 20376b39f..d451dda2f 100644 --- a/src/SmartHopper.Providers.OpenAI/OpenAIProvider.cs +++ b/src/SmartHopper.Providers.OpenAI/OpenAIProvider.cs @@ -28,19 +28,19 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Batch; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.JsonSchemas; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Streaming; -using SmartHopper.Infrastructure.Utilities; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Streaming; +using SmartHopper.ProviderSdk.Utilities; namespace SmartHopper.Providers.OpenAI { diff --git a/src/SmartHopper.Providers.OpenAI/OpenAIProviderFactory.cs b/src/SmartHopper.Providers.OpenAI/OpenAIProviderFactory.cs index 43830e040..462fe976c 100644 --- a/src/SmartHopper.Providers.OpenAI/OpenAIProviderFactory.cs +++ b/src/SmartHopper.Providers.OpenAI/OpenAIProviderFactory.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.OpenAI { diff --git a/src/SmartHopper.Providers.OpenAI/OpenAIProviderModels.cs b/src/SmartHopper.Providers.OpenAI/OpenAIProviderModels.cs index 155cd4f5a..1c769e019 100644 --- a/src/SmartHopper.Providers.OpenAI/OpenAIProviderModels.cs +++ b/src/SmartHopper.Providers.OpenAI/OpenAIProviderModels.cs @@ -21,10 +21,10 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.OpenAI { @@ -60,58 +60,86 @@ public override Task> RetrieveModels() Model = "gpt-5.6-luna", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, Rank = 10000, ContextLimit = 1050000, Created = new DateTime(2026, 7, 9), Pricing = new AIModelPricing + { + Prompt = 0.0000002m, + Completion = 0.0000012m, + InputCacheRead = 0.00000002m, + InputCacheWrite = 0.00000025m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing { Prompt = 0.0000001m, - Completion = 0.0000006000000000000001m, + Completion = 0.0000006m, InputCacheRead = 0.00000001m, - InputCacheWrite = 0.00000012500000000000002m, - WebSearch = 0.005m, + WebSearch = 0.01m, }, + Aliases = new List { "gpt-5.6-luna:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "gpt-5.6-terra", + Model = "gpt-5.6-sol", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, Rank = 9995, ContextLimit = 1050000, Created = new DateTime(2026, 7, 9), Pricing = new AIModelPricing { - Prompt = 0.00000100000000000000015m, - Completion = 0.000006m, + Prompt = 0.000002m, + Completion = 0.00001m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.0000025m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000005m, InputCacheRead = 0.0000001m, InputCacheWrite = 0.00000125m, - WebSearch = 0.005m, + WebSearch = 0.01m, }, + Aliases = new List { "gpt-5.6-sol:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "gpt-5.6-sol", + Model = "gpt-5.6-terra", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, Rank = 9990, ContextLimit = 1050000, Created = new DateTime(2026, 7, 9), Pricing = new AIModelPricing { - Prompt = 0.000005m, - Completion = 0.00003m, - InputCacheRead = 0.0000005m, - InputCacheWrite = 0.00000625m, + Prompt = 0.000002m, + Completion = 0.000012m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.0000025m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000006m, + InputCacheRead = 0.0000001m, + WebSearch = 0.01m, + }, + Aliases = new List { "gpt-5.6-terra:batch" }, }, @@ -159,26 +187,6 @@ public override Task> RetrieveModels() Aliases = new List { "gpt-5.4-mini", "gpt-5.4-mini-latest" }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "gpt-5.3-chat-latest", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9975, - ContextLimit = 400000, - Created = new DateTime(2026, 3, 3), - Pricing = new AIModelPricing - { - Prompt = 0.00000175m, - Completion = 0.000014m, - InputCacheRead = 0.000000175m, - WebSearch = 0.01m, - }, - Aliases = new List { "gpt-5.3-chat" }, - }, - new AIModelCapabilities { Provider = provider, @@ -186,7 +194,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9970, + Rank = 9975, ContextLimit = 400000, Created = new DateTime(2026, 2, 24), Pricing = new AIModelPricing @@ -205,7 +213,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9965, + Rank = 9970, ContextLimit = 1050000, Created = new DateTime(2026, 3, 5), Pricing = new AIModelPricing @@ -225,7 +233,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9960, + Rank = 9965, ContextLimit = 1050000, Created = new DateTime(2026, 4, 24), Pricing = new AIModelPricing @@ -245,7 +253,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9955, + Rank = 9960, ContextLimit = 1050000, Created = new DateTime(2026, 4, 24), Pricing = new AIModelPricing @@ -264,7 +272,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9950, + Rank = 9955, ContextLimit = 1050000, Created = new DateTime(2026, 3, 5), Pricing = new AIModelPricing @@ -280,25 +288,6 @@ public override Task> RetrieveModels() // Released between November 2025 and February 2026 - new AIModelCapabilities - { - Provider = provider, - Model = "gpt-5.1-codex-mini", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9945, - ContextLimit = 400000, - Created = new DateTime(2025, 11, 13), - Pricing = new AIModelPricing - { - Prompt = 0.00000025m, - Completion = 0.000002m, - InputCacheRead = 0.00000003m, - WebSearch = 0.01m, - }, - }, - new AIModelCapabilities { Provider = provider, @@ -307,7 +296,7 @@ public override Task> RetrieveModels() Default = AICapability.Text2Speech | AICapability.Speech2Text, SupportsStreaming = false, Verified = false, - Rank = 9940, + Rank = 9950, ContextLimit = 128000, Created = new DateTime(2026, 1, 19), Pricing = new AIModelPricing @@ -327,7 +316,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9935, + Rank = 9945, ContextLimit = 400000, Created = new DateTime(2025, 12, 4), Pricing = new AIModelPricing @@ -339,45 +328,6 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "gpt-5.1-2025-11-13", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9930, - ContextLimit = 400000, - Created = new DateTime(2025, 11, 13), - Pricing = new AIModelPricing - { - Prompt = 0.00000125m, - Completion = 0.00001m, - InputCacheRead = 0.000000125m, - WebSearch = 0.01m, - }, - Aliases = new List { "gpt-5.1", "gpt-5.1-latest" }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "gpt-5.1-codex", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9925, - ContextLimit = 400000, - Created = new DateTime(2025, 11, 13), - Pricing = new AIModelPricing - { - Prompt = 0.00000125m, - Completion = 0.00001m, - InputCacheRead = 0.00000013m, - WebSearch = 0.01m, - }, - }, - new AIModelCapabilities { Provider = provider, @@ -385,7 +335,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9920, + Rank = 9940, ContextLimit = 400000, Created = new DateTime(2026, 1, 14), Pricing = new AIModelPricing @@ -404,7 +354,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9915, + Rank = 9935, ContextLimit = 128000, Created = new DateTime(2025, 12, 10), Pricing = new AIModelPricing @@ -424,7 +374,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9910, + Rank = 9930, ContextLimit = 400000, Created = new DateTime(2025, 12, 10), Pricing = new AIModelPricing @@ -444,7 +394,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.FunctionCalling, SupportsStreaming = false, Verified = false, - Rank = 9905, + Rank = 9925, ContextLimit = 128000, Created = new DateTime(2026, 1, 19), Pricing = new AIModelPricing @@ -464,7 +414,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9900, + Rank = 9920, ContextLimit = 400000, Created = new DateTime(2025, 12, 10), Pricing = new AIModelPricing @@ -483,62 +433,59 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "gpt-5-mini-2025-08-07", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, - Default = AICapability.Text2Text | AICapability.ToolChat | AICapability.ReasoningChat | AICapability.ToolReasoningChat | AICapability.Text2Json | AICapability.Image2Text, + Model = "gpt-5.1-codex-mini", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, - Verified = true, - Rank = 9895, + Verified = false, + Rank = 9915, ContextLimit = 400000, - Created = new DateTime(2025, 8, 7), + Created = new DateTime(2025, 11, 13), Pricing = new AIModelPricing { Prompt = 0.00000025m, Completion = 0.000002m, - InputCacheRead = 0.000000025m, + InputCacheRead = 0.00000003m, WebSearch = 0.01m, }, - Aliases = new List { "gpt-5-mini", "gpt-5-mini-latest" }, }, new AIModelCapabilities { Provider = provider, - Model = "gpt-5-nano-2025-08-07", + Model = "gpt-5.1-2025-11-13", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9890, + Rank = 9910, ContextLimit = 400000, - Created = new DateTime(2025, 8, 7), + Created = new DateTime(2025, 11, 13), Pricing = new AIModelPricing { - Prompt = 0.00000005m, - Completion = 0.0000004m, - InputCacheRead = 0.000000005m, + Prompt = 0.00000125m, + Completion = 0.00001m, + InputCacheRead = 0.000000125m, WebSearch = 0.01m, }, - Aliases = new List { "gpt-5-nano", "gpt-5-nano-latest" }, + Aliases = new List { "gpt-5.1", "gpt-5.1-latest" }, }, new AIModelCapabilities { Provider = provider, - Model = "gpt-5-2025-08-07", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, + Model = "gpt-5.1-codex", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9885, + Rank = 9905, ContextLimit = 400000, - Created = new DateTime(2025, 8, 7), + Created = new DateTime(2025, 11, 13), Pricing = new AIModelPricing { Prompt = 0.00000125m, Completion = 0.00001m, - InputCacheRead = 0.000000125m, + InputCacheRead = 0.00000013m, WebSearch = 0.01m, }, - Aliases = new List { "gpt-5", "gpt-5-latest" }, }, new AIModelCapabilities @@ -548,7 +495,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9880, + Rank = 9900, ContextLimit = 400000, Created = new DateTime(2025, 10, 6), Pricing = new AIModelPricing @@ -564,6 +511,67 @@ public override Task> RetrieveModels() // Released between May 2025 and August 2025 + new AIModelCapabilities + { + Provider = provider, + Model = "gpt-5-mini-2025-08-07", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, + Default = AICapability.Text2Text | AICapability.ToolChat | AICapability.ReasoningChat | AICapability.ToolReasoningChat | AICapability.Text2Json | AICapability.Image2Text, + SupportsStreaming = true, + Verified = true, + Rank = 9895, + ContextLimit = 400000, + Created = new DateTime(2025, 8, 7), + Pricing = new AIModelPricing + { + Prompt = 0.00000025m, + Completion = 0.000002m, + InputCacheRead = 0.000000025m, + WebSearch = 0.01m, + }, + Aliases = new List { "gpt-5-mini", "gpt-5-mini-latest" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "gpt-5-nano-2025-08-07", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsStreaming = true, + Verified = false, + Rank = 9890, + ContextLimit = 400000, + Created = new DateTime(2025, 8, 7), + Pricing = new AIModelPricing + { + Prompt = 0.00000005m, + Completion = 0.0000004m, + InputCacheRead = 0.000000005m, + WebSearch = 0.01m, + }, + Aliases = new List { "gpt-5-nano", "gpt-5-nano-latest" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "gpt-5-2025-08-07", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsStreaming = true, + Verified = false, + Rank = 9885, + ContextLimit = 400000, + Created = new DateTime(2025, 8, 7), + Pricing = new AIModelPricing + { + Prompt = 0.00000125m, + Completion = 0.00001m, + InputCacheRead = 0.000000125m, + WebSearch = 0.01m, + }, + Aliases = new List { "gpt-5", "gpt-5-latest" }, + }, + new AIModelCapabilities { Provider = provider, @@ -571,7 +579,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9875, + Rank = 9880, ContextLimit = 200000, Created = new DateTime(2025, 6, 10), Pricing = new AIModelPricing @@ -594,7 +602,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling, SupportsStreaming = true, Verified = false, - Rank = 9870, + Rank = 9875, ContextLimit = 1047576, Created = new DateTime(2025, 4, 14), Pricing = new AIModelPricing @@ -614,7 +622,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling, SupportsStreaming = true, Verified = false, - Rank = 9865, + Rank = 9870, ContextLimit = 1047576, Created = new DateTime(2025, 4, 14), Pricing = new AIModelPricing @@ -634,7 +642,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9860, + Rank = 9865, ContextLimit = 200000, Created = new DateTime(2025, 4, 16), Pricing = new AIModelPricing @@ -654,7 +662,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9855, + Rank = 9860, ContextLimit = 200000, Created = new DateTime(2025, 4, 16), Pricing = new AIModelPricing @@ -674,7 +682,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling, SupportsStreaming = true, Verified = false, - Rank = 9850, + Rank = 9855, ContextLimit = 1047576, Created = new DateTime(2025, 4, 14), Pricing = new AIModelPricing @@ -694,7 +702,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9845, + Rank = 9850, ContextLimit = 200000, Created = new DateTime(2025, 3, 19), Pricing = new AIModelPricing @@ -717,7 +725,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9840, + Rank = 9845, ContextLimit = 200000, Created = new DateTime(2025, 1, 31), Pricing = new AIModelPricing @@ -737,7 +745,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9835, + Rank = 9840, ContextLimit = 200000, Created = new DateTime(2024, 12, 17), Pricing = new AIModelPricing @@ -752,29 +760,6 @@ public override Task> RetrieveModels() - // Released between August 2024 and November 2024 - - new AIModelCapabilities - { - Provider = provider, - Model = "gpt-4o-2024-08-06", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 9830, - ContextLimit = 128000, - Created = new DateTime(2024, 8, 6), - Pricing = new AIModelPricing - { - Prompt = 0.0000025m, - Completion = 0.00001m, - InputCacheRead = 0.00000125m, - }, - Aliases = new List { "gpt-4o-latest", "gpt-4o" }, - }, - - - // Released before August 2024 or unknown release date new AIModelCapabilities @@ -784,7 +769,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.ImageInput | AICapability.JsonOutput | AICapability.FunctionCalling, SupportsStreaming = true, Verified = false, - Rank = 9825, + Rank = 9835, ContextLimit = 128000, Created = new DateTime(2024, 7, 18), Pricing = new AIModelPricing @@ -803,7 +788,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, Verified = false, - Rank = 9820, + Rank = 9830, ContextLimit = 4095, Created = new DateTime(2023, 9, 28), Pricing = new AIModelPricing @@ -820,7 +805,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, Verified = false, - Rank = 9815, + Rank = 9825, ContextLimit = 16385, Created = new DateTime(2023, 8, 28), Pricing = new AIModelPricing @@ -830,6 +815,25 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "gpt-4o-2024-08-06", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsStreaming = true, + Verified = false, + Rank = 9820, + ContextLimit = 128000, + Created = new DateTime(2024, 8, 6), + Pricing = new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.00001m, + InputCacheRead = 0.00000125m, + }, + Aliases = new List { "gpt-4o-latest", "gpt-4o" }, + }, + new AIModelCapabilities { Provider = provider, @@ -837,7 +841,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, Verified = false, - Rank = 9810, + Rank = 9815, ContextLimit = 128000, Created = new DateTime(2024, 5, 13), Pricing = new AIModelPricing @@ -854,7 +858,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling, SupportsStreaming = true, Verified = false, - Rank = 9805, + Rank = 9810, ContextLimit = 128000, Aliases = new List { "chat" }, }, @@ -865,7 +869,7 @@ public override Task> RetrieveModels() Model = "chatgpt-image-latest", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput, Verified = false, - Rank = 9800, + Rank = 9805, ContextLimit = 128000, Aliases = new List { "chatgpt-image" }, }, @@ -877,7 +881,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, Verified = false, - Rank = 9795, + Rank = 9800, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -895,7 +899,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.AudioInput | AICapability.TextOutput, SupportsStreaming = false, Verified = false, - Rank = 9790, + Rank = 9795, ContextLimit = 16000, }, @@ -906,7 +910,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.AudioInput | AICapability.TextOutput, SupportsStreaming = false, Verified = false, - Rank = 9785, + Rank = 9790, ContextLimit = 16000, Aliases = new List { "gpt-4o-mini-transcribe", "gpt-4o-mini-transcribe-latest" }, }, @@ -918,7 +922,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.AudioOutput, SupportsStreaming = false, Verified = false, - Rank = 9780, + Rank = 9785, ContextLimit = 2000, }, @@ -929,7 +933,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.AudioOutput, SupportsStreaming = false, Verified = false, - Rank = 9775, + Rank = 9780, ContextLimit = 2000, Aliases = new List { "gpt-4o-mini-tts", "gpt-4o-mini-tts-latest" }, }, @@ -941,7 +945,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, Verified = false, - Rank = 9770, + Rank = 9775, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -959,7 +963,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.AudioInput | AICapability.TextOutput, SupportsStreaming = false, Verified = false, - Rank = 9765, + Rank = 9770, ContextLimit = 16000, }, @@ -970,7 +974,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.AudioInput | AICapability.TextOutput, SupportsStreaming = false, Verified = false, - Rank = 9760, + Rank = 9765, ContextLimit = 16000, }, @@ -981,7 +985,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.ImageInput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9755, + Rank = 9760, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -999,16 +1003,25 @@ public override Task> RetrieveModels() Model = "gpt-5-codex", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9750, + Rank = 9755, ContextLimit = 400000, Pricing = new AIModelPricing { - Prompt = 0.00000125m, - Completion = 0.00001m, - InputCacheRead = 0.000000125m, + Prompt = 0.000000625m, + Completion = 0.000005m, + InputCacheRead = 0.0000000625m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000625m, + Completion = 0.000005m, + InputCacheRead = 0.0000000625m, WebSearch = 0.01m, }, + Aliases = new List { "gpt-5-codex:batch" }, }, new AIModelCapabilities @@ -1018,7 +1031,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, Verified = false, - Rank = 9745, + Rank = 9750, ContextLimit = 400000, Aliases = new List { "gpt-5-search-api", "gpt-5-search-api-latest" }, }, @@ -1030,7 +1043,7 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, Verified = false, - Rank = 9740, + Rank = 9745, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -1042,6 +1055,25 @@ public override Task> RetrieveModels() Aliases = new List { "gpt-5.1-chat" }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "gpt-5.3-chat-latest", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsStreaming = true, + Verified = false, + Rank = 9740, + ContextLimit = 400000, + Pricing = new AIModelPricing + { + Prompt = 0.00000175m, + Completion = 0.000014m, + InputCacheRead = 0.000000175m, + WebSearch = 0.01m, + }, + Aliases = new List { "gpt-5.3-chat" }, + }, + new AIModelCapabilities { Provider = provider, @@ -1315,6 +1347,7 @@ public override Task> RetrieveModels() Model = "gpt-3.5-turbo", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = false, + SupportsBatch = true, Verified = false, Deprecated = true, Rank = -10, @@ -1325,6 +1358,13 @@ public override Task> RetrieveModels() Prompt = 0.0000005m, Completion = 0.0000015m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000025m, + Completion = 0.00000075m, + WebSearch = 0.01m, + }, + Aliases = new List { "gpt-3.5-turbo:batch" }, }, new AIModelCapabilities diff --git a/src/SmartHopper.Providers.OpenAI/OpenAIProviderSettings.cs b/src/SmartHopper.Providers.OpenAI/OpenAIProviderSettings.cs index c78f0fdb0..02204834f 100644 --- a/src/SmartHopper.Providers.OpenAI/OpenAIProviderSettings.cs +++ b/src/SmartHopper.Providers.OpenAI/OpenAIProviderSettings.cs @@ -20,9 +20,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Dialogs; -using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Providers.OpenAI { @@ -168,7 +169,7 @@ public override bool ValidateSettings(Dictionary settings) if (showErrorDialogs) { - StyledMessageDialog.ShowError("Reasoning effort must be low, medium, or high.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Reasoning effort must be low, medium, or high.")); } return false; @@ -189,7 +190,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Max Tokens must be greater than 0.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Max Tokens must be greater than 0.")); } return false; @@ -209,7 +210,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Temperature must be between 0.0 and 2.0.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Temperature must be between 0.0 and 2.0.")); } return false; diff --git a/src/SmartHopper.Providers.OpenAI/SmartHopper.Providers.OpenAI.csproj b/src/SmartHopper.Providers.OpenAI/SmartHopper.Providers.OpenAI.csproj index df832c244..c57014b6f 100644 --- a/src/SmartHopper.Providers.OpenAI/SmartHopper.Providers.OpenAI.csproj +++ b/src/SmartHopper.Providers.OpenAI/SmartHopper.Providers.OpenAI.csproj @@ -36,7 +36,7 @@ - + diff --git a/src/SmartHopper.Providers.OpenRouter/OpenRouterProvider.Batch.cs b/src/SmartHopper.Providers.OpenRouter/OpenRouterProvider.Batch.cs new file mode 100644 index 000000000..743630d26 --- /dev/null +++ b/src/SmartHopper.Providers.OpenRouter/OpenRouterProvider.Batch.cs @@ -0,0 +1,512 @@ +/* + * SmartHopper - AI-powered Grasshopper Plugin + * Copyright (C) 2024-2026 Marc Roca Musach + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.Diagnostics; + +namespace SmartHopper.Providers.OpenRouter +{ + public sealed partial class OpenRouterProvider : IAIBatchProvider + { + /// + /// Builds an absolute URL for the OpenRouter beta batch endpoint. + /// The batch API lives under /api/beta/batches, which is not under + /// the provider's default /api/v1 base path. + /// + /// Path relative to https://openrouter.ai/, without a leading slash. + /// The absolute batch URL. + private Uri BuildBatchUrl(string relativePath) + { + var authority = this.DefaultServerUrl.GetLeftPart(UriPartial.Authority); + var baseUri = new Uri(authority + "/", UriKind.Absolute); + return new Uri(baseUri, relativePath); + } + + /// + /// Strips the OpenRouter :batch suffix from a model slug. + /// This keeps old definitions and user inputs that selected the + /// :batch alias working with both chat and batch endpoints. + /// + /// The model slug, possibly ending with :batch. + /// The base model slug without a :batch suffix. + private static string StripBatchSuffix(string model) + { + if (string.IsNullOrWhiteSpace(model)) + { + return model; + } + + if (model.EndsWith(":batch", StringComparison.OrdinalIgnoreCase)) + { + return model.Substring(0, model.Length - 6); + } + + return model; + } + + /// + /// Maps an internal request endpoint to the shape expected by the + /// OpenRouter batch API. The batch endpoint must start with /v1/. + /// + /// The endpoint from . + /// The batch-level endpoint string. + private static string ToBatchEndpoint(string endpoint) + { + if (string.IsNullOrWhiteSpace(endpoint)) + { + return "/v1/chat/completions"; + } + + var normalized = endpoint.TrimStart('/'); + + if (endpoint.StartsWith("/v1/", StringComparison.OrdinalIgnoreCase)) + { + return endpoint; + } + + switch (normalized.ToLowerInvariant()) + { + case "chat/completions": + return "/v1/chat/completions"; + case "responses": + return "/v1/responses"; + case "messages": + return "/v1/messages"; + case "embeddings": + return "/v1/embeddings"; + default: + return "/v1/" + normalized; + } + } + + /// + /// Validates that a batch request body only contains text content. + /// OpenRouter's batch API currently rejects image, audio, video and file + /// parts, as well as non-text output requests. + /// + /// The encoded request body. + /// Thrown when the request is not text-only. + private static void ValidateBatchTextOnly(JObject body) + { + if (body == null) + { + return; + } + + var messages = body["messages"] as JArray; + if (messages != null) + { + foreach (var message in messages.OfType()) + { + var content = message["content"]; + if (content is JArray parts) + { + foreach (var part in parts.OfType()) + { + var type = part["type"]?.ToString() ?? string.Empty; + if (string.Equals(type, "image_url", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "audio_url", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "video_url", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "file", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"[OpenRouter] Batch requests do not support '{type}' content. Use the synchronous API for multimodal requests."); + } + + if (part["image_url"] != null + || part["audio_url"] != null + || part["video_url"] != null + || part["file"] != null) + { + throw new InvalidOperationException("[OpenRouter] Batch requests do not support multimodal content. Use the synchronous API for multimodal requests."); + } + } + } + } + } + + if (body["modalities"] != null + || body["audio"] != null + || body["image_config"] != null) + { + throw new InvalidOperationException("[OpenRouter] Batch requests do not support non-text output configurations (modalities, audio, image_config)."); + } + } + + /// + public async Task SubmitBatchAsync(IReadOnlyList<(string CustomId, AIRequestCall Request)> items, CancellationToken cancellationToken = default) + { + if (items == null || items.Count == 0) + { + throw new ArgumentException("At least one batch item is required.", nameof(items)); + } + + string? batchEndpoint = null; + string? batchModel = null; + var requestArray = new JArray(); + var customIds = new List(); + string? firstEncodedBody = null; + + foreach (var (customId, request) in items) + { + // Normalize :batch alias to the base model slug before encoding. + request.Model = StripBatchSuffix(request.Model); + + var preparedRequest = this.PreCall(request); + var encodedBody = this.Encode(preparedRequest); + if (firstEncodedBody == null) + { + firstEncodedBody = encodedBody; + } + + var bodyObj = JObject.Parse(encodedBody); + + // OpenRouter batch is text-only and uses one endpoint/model for the whole batch. + ValidateBatchTextOnly(bodyObj); + + var endpoint = ToBatchEndpoint(preparedRequest.Endpoint); + if (batchEndpoint == null) + { + batchEndpoint = endpoint; + } + else if (!string.Equals(batchEndpoint, endpoint, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"[OpenRouter] All batch items must use the same endpoint. Expected '{batchEndpoint}', found '{endpoint}'."); + } + + if (batchModel == null) + { + batchModel = preparedRequest.Model; + } + else if (!string.Equals(batchModel, preparedRequest.Model, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"[OpenRouter] All batch items must use the same model. Expected '{batchModel}', found '{preparedRequest.Model}'."); + } + + var requestItem = new JObject + { + ["custom_id"] = customId, + ["body"] = bodyObj, + }; + requestArray.Add(requestItem); + customIds.Add(customId); + } + + // OpenRouter streams the input and requires endpoint/model to appear + // before the (potentially very large) requests array. + var batchBody = new JObject + { + ["endpoint"] = batchEndpoint, + ["model"] = batchModel, + ["completion_window"] = "24h", + ["requests"] = requestArray, + }; + + var apiKey = this.GetApiKey(); + using var client = this.CreateBatchHttpClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Add("X-Title", "SmartHopper"); + client.DefaultRequestHeaders.Add("Referer", "https://smarthopper.xyz"); + + var batchUrl = this.BuildBatchUrl("api/beta/batches"); + var content = new StringContent(batchBody.ToString(Formatting.None), Encoding.UTF8, "application/json"); + var response = await client.PostAsync(batchUrl, content, cancellationToken).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (!response.IsSuccessStatusCode && (int)response.StatusCode != 202) + { + throw new InvalidOperationException($"[OpenRouter] Batch submission failed ({(int)response.StatusCode}): {responseString}"); + } + + var result = JObject.Parse(responseString); + var batchId = result["id"]?.ToString() + ?? throw new InvalidOperationException("[OpenRouter] Batch submission response missing 'id'."); + + Debug.WriteLine($"[OpenRouter] Batch submitted: batchId={batchId}, count={items.Count}"); + + return new AIBatchSubmission(batchId, this.Name, firstEncodedBody ?? string.Empty, (IReadOnlyList)customIds.AsReadOnly()); + } + + /// + public async Task GetBatchStatusAsync(AIBatchSubmission submission, CancellationToken cancellationToken = default) + { + if (submission == null) + { + throw new ArgumentNullException(nameof(submission)); + } + + var json = await this.GetBatchJsonAsync(submission.BatchId, cancellationToken).ConfigureAwait(false); + if (json == null) + { + return new AIBatchStatus(submission.BatchId, AIBatchState.Failed, "OpenRouter batch status response was empty."); + } + + var status = json["status"]?.ToString() ?? string.Empty; + Debug.WriteLine($"[OpenRouter] Batch status check: id={submission.BatchId}, status={status}"); + + switch (status.ToLowerInvariant()) + { + case "validating": + case "in_progress": + case "finalizing": + { + var requestCounts = json["request_counts"] as JObject; + int? completedCount = requestCounts?["completed"]?.Value(); + return new AIBatchStatus(submission.BatchId, AIBatchState.InProgress, completedCount: completedCount); + } + + case "completed": + { + var results = json["results"]; + if (results == null || !results.HasValues) + { + return new AIBatchStatus(submission.BatchId, AIBatchState.Failed, "Batch completed but no results were returned."); + } + + var parsed = this.ParseBatchResultsFiles(new[] { results.ToString(Formatting.None) }, submission.BatchId); + if ((parsed.Results?.Count ?? 0) == 0 && (parsed.Messages?.Count ?? 0) == 0) + { + return new AIBatchStatus(submission.BatchId, AIBatchState.Failed, "No results found in completed batch."); + } + + return parsed; + } + + case "failed": + return new AIBatchStatus(submission.BatchId, AIBatchState.Failed, json["error"]?.ToString() ?? string.Empty); + + case "expired": + return new AIBatchStatus(submission.BatchId, AIBatchState.Expired); + + case "cancelling": + case "cancelled": + return new AIBatchStatus(submission.BatchId, AIBatchState.Cancelled); + + default: + return new AIBatchStatus(submission.BatchId, AIBatchState.Submitted); + } + } + + /// + public async Task CancelBatchAsync(AIBatchSubmission submission, CancellationToken cancellationToken = default) + { + if (submission == null) + { + throw new ArgumentNullException(nameof(submission)); + } + + var apiKey = this.GetApiKey(); + using var client = this.CreateBatchHttpClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Add("X-Title", "SmartHopper"); + client.DefaultRequestHeaders.Add("Referer", "https://smarthopper.xyz"); + + var cancelUrl = this.BuildBatchUrl($"api/beta/batches/{submission.BatchId}/cancel"); + var response = await client.PostAsync(cancelUrl, null, cancellationToken).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + Debug.WriteLine($"[OpenRouter] Batch cancel failed ({(int)response.StatusCode}): {content}"); + } + else + { + Debug.WriteLine($"[OpenRouter] Batch {submission.BatchId} cancel accepted."); + } + } + + /// + public async Task> DownloadBatchResultsAsync(AIBatchSubmission submission, CancellationToken cancellationToken = default) + { + if (submission == null) + { + throw new ArgumentNullException(nameof(submission)); + } + + var json = await this.GetBatchJsonAsync(submission.BatchId, cancellationToken).ConfigureAwait(false); + if (json == null) + { + return Array.Empty(); + } + + var results = json["results"]; + if (results != null && results.HasValues) + { + return new[] { results.ToString(Formatting.None) }; + } + + return Array.Empty(); + } + + /// + public AIBatchStatus ParseBatchResultsFiles(IReadOnlyList fileContents, string batchId = null) + { + if (fileContents == null || fileContents.Count == 0) + { + return new AIBatchStatus(batchId, AIBatchState.Failed, "No batch result contents provided."); + } + + var merged = new Dictionary(); + var messages = new List(); + + foreach (var content in fileContents) + { + AIBatchStatusMerge.MergeInto(ParseSingleBatchResultFile(content, batchId), merged, messages); + } + + return new AIBatchStatus( + batchId, + new System.Collections.ObjectModel.ReadOnlyDictionary(merged), + messages.AsReadOnly()); + } + + /// + /// Fetches the raw JSON for a batch from the OpenRouter status endpoint. + /// + /// The OpenRouter batch id. + /// Cancellation token. + /// The parsed batch object, or null if the request failed. + private async Task GetBatchJsonAsync(string batchId, CancellationToken cancellationToken) + { + var apiKey = this.GetApiKey(); + using var client = this.CreateBatchHttpClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Add("X-Title", "SmartHopper"); + client.DefaultRequestHeaders.Add("Referer", "https://smarthopper.xyz"); + + var statusUrl = this.BuildBatchUrl($"api/beta/batches/{batchId}"); + var response = await client.GetAsync(statusUrl, cancellationToken).ConfigureAwait(false); + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + Debug.WriteLine($"[OpenRouter] Batch status fetch failed ({(int)response.StatusCode}): {content}"); + return null; + } + + return JObject.Parse(content); + } + + /// + /// Parses a single OpenRouter batch result payload. The payload can be the + /// inline results array returned by the status endpoint, or a + /// previously saved JSON array of result items. + /// + /// JSON content containing the results array (or a batch object with a results property). + /// Optional batch id for diagnostics. + /// A parsed for the single file. + private static AIBatchStatus ParseSingleBatchResultFile(string content, string batchId) + { + var results = new Dictionary(); + var messages = new List(); + + if (string.IsNullOrWhiteSpace(content)) + { + return new AIBatchStatus(batchId, results, messages); + } + + JArray resultArray; + var token = JToken.Parse(content); + if (token is JArray array) + { + resultArray = array; + } + else if (token is JObject obj && obj["results"] is JArray nested) + { + resultArray = nested; + } + else + { + messages.Add(new SHRuntimeMessage( + SHRuntimeMessageSeverity.Error, + SHRuntimeMessageOrigin.Provider, + SHMessageCode.BatchItemError, + "Batch result payload is not a results array or a batch object with a results array.")); + return new AIBatchStatus(batchId, results, messages); + } + + foreach (var item in resultArray.OfType()) + { + var customId = item["custom_id"]?.ToString(); + if (string.IsNullOrWhiteSpace(customId)) + { + continue; + } + + var error = item["error"]; + if (error != null) + { + var errorMsg = error["message"]?.ToString() ?? error.ToString(); + messages.Add(new SHRuntimeMessage( + SHRuntimeMessageSeverity.Error, + SHRuntimeMessageOrigin.Provider, + SHMessageCode.BatchItemError, + $"Batch item {customId}: {errorMsg}")); + continue; + } + + var responseObj = item["response"] as JObject; + var statusCode = responseObj?["status_code"]?.Value() ?? 0; + var responseBody = responseObj?["body"] as JObject; + + if (statusCode >= 200 && statusCode < 300 && responseBody != null) + { + results[customId] = responseBody; + + // Surface non-standard finish reasons as warnings. + var choices = responseBody["choices"] as JArray; + var firstChoice = choices?.FirstOrDefault() as JObject; + var finishReason = firstChoice?["finish_reason"]?.ToString() + ?? responseBody["status"]?.ToString(); + + if (!string.IsNullOrEmpty(finishReason) && finishReason != "stop") + { + messages.Add(new SHRuntimeMessage( + SHRuntimeMessageSeverity.Warning, + SHRuntimeMessageOrigin.Provider, + SHMessageCode.BatchItemFinishReason, + $"Batch item {customId}: completed with finish_reason='{finishReason}'")); + } + } + else + { + var errorText = responseBody?["error"]?.ToString() + ?? (statusCode > 0 ? $"HTTP {statusCode}" : "Unknown batch item error"); + messages.Add(new SHRuntimeMessage( + SHRuntimeMessageSeverity.Error, + SHRuntimeMessageOrigin.Provider, + SHMessageCode.BatchItemError, + $"Batch item {customId}: {errorText}")); + } + } + + return new AIBatchStatus(batchId, results, messages); + } + } +} diff --git a/src/SmartHopper.Providers.OpenRouter/OpenRouterProvider.cs b/src/SmartHopper.Providers.OpenRouter/OpenRouterProvider.cs index 2e1e77d2e..217b753b6 100644 --- a/src/SmartHopper.Providers.OpenRouter/OpenRouterProvider.cs +++ b/src/SmartHopper.Providers.OpenRouter/OpenRouterProvider.cs @@ -29,16 +29,17 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core; -using SmartHopper.Infrastructure.AICall.Core.Base; -using SmartHopper.Infrastructure.AICall.Core.Interactions; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AICall.JsonSchemas; -using SmartHopper.Infrastructure.AICall.Metrics; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Diagnostics; -using SmartHopper.Infrastructure.Streaming; +using SmartHopper.ProviderSdk.AICall.Batch; +using SmartHopper.ProviderSdk.AICall.Core; +using SmartHopper.ProviderSdk.AICall.Core.Base; +using SmartHopper.ProviderSdk.AICall.Core.Interactions; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AICall.JsonSchemas; +using SmartHopper.ProviderSdk.AICall.Metrics; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Streaming; namespace SmartHopper.Providers.OpenRouter { @@ -47,7 +48,7 @@ namespace SmartHopper.Providers.OpenRouter /// Provides access to multiple AI models through a unified interface. /// Supports text and vision input; image generation is handled via the img_generate tool. /// - public sealed class OpenRouterProvider : AIProvider + public sealed partial class OpenRouterProvider : AIProvider, IAIBatchProvider { private OpenRouterProvider() { @@ -115,6 +116,14 @@ public override Image Icon /// public override AIRequestCall PreCall(AIRequestCall request) { + // Normalize :batch aliases to the base model slug before any further processing. + if (request == null) + { + return request!; + } + + request.Model = StripBatchSuffix(request.Model); + // Base pipeline first request = base.PreCall(request); @@ -643,7 +652,7 @@ public override List Decode(JObject response) reasoning: string.IsNullOrWhiteSpace(reasoning) ? null : reasoning); // Extract metrics (tokens, model, finish reason) if present - var metrics = new Infrastructure.AICall.Metrics.AIMetrics + var metrics = new ProviderSdk.AICall.Metrics.AIMetrics { Provider = this.Name, Model = response["model"]?.ToString(), diff --git a/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderFactory.cs b/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderFactory.cs index 39191cf25..f44a75ceb 100644 --- a/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderFactory.cs +++ b/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderFactory.cs @@ -16,7 +16,7 @@ * along with this library; if not, see . */ -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.OpenRouter { diff --git a/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderModels.cs b/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderModels.cs index f0c02fa73..78de959c7 100644 --- a/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderModels.cs +++ b/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderModels.cs @@ -22,10 +22,10 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using SmartHopper.Infrastructure.AICall.Core.Requests; -using SmartHopper.Infrastructure.AICall.Core.Returns; -using SmartHopper.Infrastructure.AIModels; -using SmartHopper.Infrastructure.AIProviders; +using SmartHopper.ProviderSdk.AICall.Core.Requests; +using SmartHopper.ProviderSdk.AICall.Core.Returns; +using SmartHopper.ProviderSdk.AIModels; +using SmartHopper.ProviderSdk.AIProviders; namespace SmartHopper.Providers.OpenRouter { @@ -57,6 +57,7 @@ public override Task> RetrieveModels() Provider = provider, Model = "openrouter/auto-beta", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, Rank = 10000, ContextLimit = 2000000, @@ -73,6 +74,7 @@ public override Task> RetrieveModels() Provider = provider, Model = "openrouter/fusion", Capabilities = AICapability.TextInput | AICapability.TextOutput, + SupportsBatch = false, Verified = false, Rank = 9995, ContextLimit = 1000000, @@ -87,12 +89,49 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "inclusionai/ling-3.0-flash:free", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + Model = "stealth/ox-alpha", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, Rank = 9990, + ContextLimit = 1048576, + Created = new DateTime(2026, 8, 20), + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "liquid/lfm-2.5-2.6b:free", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9985, + ContextLimit = 65536, + Created = new DateTime(2026, 8, 11), + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "nvidia/nemotron-3.5-lightning:free", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9980, + ContextLimit = 1000000, + Created = new DateTime(2026, 8, 11), + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "thinkingmachines/inkling-small:free", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9975, ContextLimit = 262144, - Created = new DateTime(2026, 7, 23), + Created = new DateTime(2026, 7, 30), }, new AIModelCapabilities @@ -100,19 +139,33 @@ public override Task> RetrieveModels() Provider = provider, Model = "poolside/laguna-s-2.1:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9985, + Rank = 9970, ContextLimit = 262144, Created = new DateTime(2026, 7, 21), }, + new AIModelCapabilities + { + Provider = provider, + Model = "thinkingmachines/inkling:free", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9965, + ContextLimit = 262144, + Created = new DateTime(2026, 7, 17), + }, + new AIModelCapabilities { Provider = provider, Model = "poolside/laguna-xs-2.1:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9980, + Rank = 9960, ContextLimit = 262144, Created = new DateTime(2026, 7, 2), }, @@ -122,19 +175,33 @@ public override Task> RetrieveModels() Provider = provider, Model = "cohere/north-mini-code:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9975, + Rank = 9955, ContextLimit = 256000, Created = new DateTime(2026, 6, 17), }, + new AIModelCapabilities + { + Provider = provider, + Model = "z-ai/glm-5.2:free", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9950, + ContextLimit = 256000, + Created = new DateTime(2026, 6, 16), + }, + new AIModelCapabilities { Provider = provider, Model = "nvidia/nemotron-3.5-content-safety:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9970, + Rank = 9945, ContextLimit = 128000, Created = new DateTime(2026, 6, 4), }, @@ -144,19 +211,39 @@ public override Task> RetrieveModels() Provider = provider, Model = "nvidia/nemotron-3-ultra-550b-a55b:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9965, + Rank = 9940, ContextLimit = 1000000, Created = new DateTime(2026, 6, 4), }, + new AIModelCapabilities + { + Provider = provider, + Model = "inclusionai/ling-3.0-flash", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9935, + ContextLimit = 262144, + Created = new DateTime(2026, 7, 23), + Pricing = new AIModelPricing + { + Prompt = 0.000000021m, + Completion = 0.000000063m, + InputCacheRead = 0.0000000042m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "nex-agi/nex-n2-mini", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9960, + Rank = 9930, ContextLimit = 262144, Created = new DateTime(2026, 6, 24), Pricing = new AIModelPricing @@ -167,13 +254,32 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "upstage/solar-pro4", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9925, + ContextLimit = 524288, + Created = new DateTime(2026, 8, 10), + Pricing = new AIModelPricing + { + Prompt = 0.00000003m, + Completion = 0.00000012m, + InputCacheRead = 0.000000006m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "poolside/laguna-xs-2.1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9955, + Rank = 9920, ContextLimit = 262144, Created = new DateTime(2026, 7, 2), Pricing = new AIModelPricing @@ -189,8 +295,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "qwen/qwen3.7-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9950, + Rank = 9915, ContextLimit = 1000000, Created = new DateTime(2026, 7, 27), Pricing = new AIModelPricing @@ -207,15 +314,33 @@ public override Task> RetrieveModels() Provider = provider, Model = "~deepseek/deepseek-v4-flash-latest", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9945, - ContextLimit = 1048576, + Rank = 9910, + ContextLimit = 1310720, Created = new DateTime(2026, 8, 1), Pricing = new AIModelPricing { - Prompt = 0.00000009m, - Completion = 0.00000018m, - InputCacheRead = 0.000000018m, + Prompt = 0.000000055m, + Completion = 0.00000014m, + InputCacheRead = 0.00000001m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "tencent/hy-mt2-1.8b", + Capabilities = AICapability.TextInput | AICapability.TextOutput, + SupportsBatch = false, + Verified = false, + Rank = 9905, + ContextLimit = 8192, + Created = new DateTime(2026, 8, 20), + Pricing = new AIModelPricing + { + Prompt = 0.000000044m, + Completion = 0.000000177m, }, }, @@ -224,15 +349,16 @@ public override Task> RetrieveModels() Provider = provider, Model = "deepseek/deepseek-v4-flash-0731", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9940, - ContextLimit = 1048576, + Rank = 9900, + ContextLimit = 1310720, Created = new DateTime(2026, 7, 31), Pricing = new AIModelPricing { - Prompt = 0.00000009m, + Prompt = 0.00000008m, Completion = 0.00000018m, - InputCacheRead = 0.000000018m, + InputCacheRead = 0.000000016m, }, }, @@ -241,8 +367,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "poolside/laguna-s-2.1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9935, + Rank = 9895, ContextLimit = 1048576, Created = new DateTime(2026, 7, 21), Pricing = new AIModelPricing @@ -256,124 +383,143 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "tencent/hy3", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "meta/muse-spark-1.2-contributor", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9930, - ContextLimit = 262144, - Created = new DateTime(2026, 7, 6), + Rank = 9890, + ContextLimit = 1048576, + Created = new DateTime(2026, 8, 21), Pricing = new AIModelPricing { - Prompt = 0.000000132m, - Completion = 0.000000528m, - InputCacheRead = 0.000000033m, + Prompt = 0.0000001m, + Completion = 0.0000002m, + InputCacheRead = 0.000000002m, + WebSearch = 0.0025m, }, }, new AIModelCapabilities { Provider = provider, - Model = "kwaipilot/kat-coder-air-v2.5", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + Model = "nvidia/nemotron-3.5-lightning", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9925, - ContextLimit = 256000, - Created = new DateTime(2026, 7, 10), + Rank = 9885, + ContextLimit = 262144, + Created = new DateTime(2026, 8, 11), Pricing = new AIModelPricing { - Prompt = 0.00000015m, - Completion = 0.0000006m, - InputCacheRead = 0.00000003m, + Prompt = 0.00000008m, + Completion = 0.0000002m, + InputCacheRead = 0.00000004m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.6-luna-pro", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "tencent/hy-mt2-30b-a3b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + SupportsBatch = false, Verified = false, - Rank = 9920, - ContextLimit = 1050000, - Created = new DateTime(2026, 7, 9), + Rank = 9880, + ContextLimit = 8192, + Created = new DateTime(2026, 8, 20), Pricing = new AIModelPricing { - Prompt = 0.0000001m, - Completion = 0.0000006000000000000001m, - InputCacheRead = 0.00000001m, - InputCacheWrite = 0.00000012500000000000002m, - WebSearch = 0.005m, + Prompt = 0.000000074m, + Completion = 0.000000295m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.6-luna", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - Default = AICapability.ToolChat | AICapability.ReasoningChat | AICapability.ToolReasoningChat, + Model = "tencent/hy-mt2-7b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + SupportsBatch = false, Verified = false, - Rank = 9915, - ContextLimit = 1050000, - Created = new DateTime(2026, 7, 9), + Rank = 9875, + ContextLimit = 8192, + Created = new DateTime(2026, 8, 19), Pricing = new AIModelPricing { - Prompt = 0.0000001m, - Completion = 0.0000006000000000000001m, - InputCacheRead = 0.00000001m, - InputCacheWrite = 0.00000012500000000000002m, - WebSearch = 0.005m, + Prompt = 0.000000074m, + Completion = 0.000000295m, }, }, new AIModelCapabilities { Provider = provider, - Model = "inclusionai/ring-2.6-1t", + Model = "tencent/hy3", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9910, + Rank = 9870, ContextLimit = 262144, - Created = new DateTime(2026, 5, 8), + Created = new DateTime(2026, 7, 6), Pricing = new AIModelPricing { - Prompt = 0.000000075m, - Completion = 0.000000625m, - InputCacheRead = 0.000000015m, + Prompt = 0.000000132m, + Completion = 0.000000528m, + InputCacheRead = 0.000000033m, }, }, new AIModelCapabilities { Provider = provider, - Model = "nex-agi/nex-n2-pro", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + Model = "kwaipilot/kat-coder-air-v2.5", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsBatch = false, Verified = false, - Rank = 9905, - ContextLimit = 262144, - Created = new DateTime(2026, 6, 8), + Rank = 9865, + ContextLimit = 256000, + Created = new DateTime(2026, 7, 10), Pricing = new AIModelPricing { - Prompt = 0.00000025m, - Completion = 0.000001m, - InputCacheRead = 0.000000025m, + Prompt = 0.00000015m, + Completion = 0.0000006m, + InputCacheRead = 0.00000003m, }, }, new AIModelCapabilities { Provider = provider, - Model = "z-ai/glm-5.2", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "deepseek/deepseek-v4-flash-vision-exp", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9900, + Rank = 9860, ContextLimit = 1048576, - Created = new DateTime(2026, 6, 16), + Created = new DateTime(2026, 8, 21), + Pricing = new AIModelPricing + { + Prompt = 0.00000022m, + Completion = 0.00000066m, + InputCacheRead = 0.000000007m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "nex-agi/nex-n2-pro", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9855, + ContextLimit = 262144, + Created = new DateTime(2026, 6, 8), Pricing = new AIModelPricing { - Prompt = 0.0000003346m, - Completion = 0.0000010516m, - InputCacheRead = 0.00000006214m, + Prompt = 0.00000025m, + Completion = 0.000001m, + InputCacheRead = 0.000000025m, }, }, @@ -382,8 +528,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "stepfun/step-3.7-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9895, + Rank = 9850, ContextLimit = 262144, Created = new DateTime(2026, 5, 28), Pricing = new AIModelPricing @@ -398,14 +545,15 @@ public override Task> RetrieveModels() { Provider = provider, Model = "thinkingmachines/inkling-small", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9890, - ContextLimit = 524288, + Rank = 9845, + ContextLimit = 1048576, Created = new DateTime(2026, 7, 30), Pricing = new AIModelPricing { - Prompt = 0.0000005m, + Prompt = 0.00000045m, Completion = 0.0000012m, InputCacheRead = 0.0000001m, }, @@ -416,8 +564,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "meituan/longcat-2.0", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9885, + Rank = 9840, ContextLimit = 1048756, Created = new DateTime(2026, 7, 20), Pricing = new AIModelPricing @@ -431,27 +580,93 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "minimax/minimax-m3", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "openai/gpt-5.6-luna-pro", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9880, - ContextLimit = 1048576, - Created = new DateTime(2026, 5, 31), + Rank = 9835, + ContextLimit = 1050000, + Created = new DateTime(2026, 7, 9), Pricing = new AIModelPricing { - Prompt = 0.0000003m, + Prompt = 0.0000002m, Completion = 0.0000012m, - InputCacheRead = 0.00000006m, + InputCacheRead = 0.00000002m, + InputCacheWrite = 0.00000025m, + WebSearch = 0.01m, }, - }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000001m, + Completion = 0.0000006m, + InputCacheRead = 0.00000001m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.6-luna-pro:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-5.6-luna", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Default = AICapability.ToolChat | AICapability.ReasoningChat | AICapability.ToolReasoningChat, + SupportsBatch = true, + Verified = false, + Rank = 9830, + ContextLimit = 1050000, + Created = new DateTime(2026, 7, 9), + Pricing = new AIModelPricing + { + Prompt = 0.0000002m, + Completion = 0.0000012m, + InputCacheRead = 0.00000002m, + InputCacheWrite = 0.00000025m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000001m, + Completion = 0.0000006m, + InputCacheRead = 0.00000001m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.6-luna:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "minimax/minimax-m3", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, + Verified = false, + Rank = 9825, + ContextLimit = 1048576, + Created = new DateTime(2026, 5, 31), + Pricing = new AIModelPricing + { + Prompt = 0.0000003m, + Completion = 0.0000012m, + InputCacheRead = 0.00000006m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000003m, + Completion = 0.0000012m, + InputCacheRead = 0.00000006m, + }, + Aliases = new List { "minimax/minimax-m3:batch" }, + }, new AIModelCapabilities { Provider = provider, Model = "qwen/qwen3.7-plus", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9875, + Rank = 9820, ContextLimit = 1000000, Created = new DateTime(2026, 6, 3), Pricing = new AIModelPricing @@ -468,8 +683,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "aion-labs/aion-3.0-mini", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9870, + Rank = 9815, ContextLimit = 131072, Created = new DateTime(2026, 7, 7), Pricing = new AIModelPricing @@ -483,59 +699,73 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "perceptron/perceptron-mk1", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, + Model = "meta/muse-glimmer-30b", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9865, - ContextLimit = 32768, - Created = new DateTime(2026, 5, 12), + Rank = 9810, + ContextLimit = 131072, + Created = new DateTime(2026, 8, 9), Pricing = new AIModelPricing { - Prompt = 0.00000015m, + Prompt = 0.00000035m, Completion = 0.0000015m, + InputCacheRead = 0.00000004m, }, }, new AIModelCapabilities { Provider = provider, - Model = "x-ai/grok-build-0.1", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "bytedance-seed/seed-2-1-turbo", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9860, - ContextLimit = 256000, - Created = new DateTime(2026, 5, 20), + Rank = 9805, + ContextLimit = 262144, + Created = new DateTime(2026, 8, 12), Pricing = new AIModelPricing { - Prompt = 0.000001m, - Completion = 0.000002m, - InputCacheRead = 0.0000002m, - WebSearch = 0.005m, + Prompt = 0.0000005m, + Completion = 0.0000025m, }, }, new AIModelCapabilities { Provider = provider, - Model = "google/gemini-3.1-flash-lite", + Model = "google/gemini-3.7-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - Default = AICapability.Speech2Text, + SupportsBatch = true, Verified = false, - Rank = 9855, + Rank = 9800, ContextLimit = 1048576, - Created = new DateTime(2026, 5, 7), + Created = new DateTime(2026, 8, 13), Pricing = new AIModelPricing { - Prompt = 0.00000025m, - Completion = 0.0000015m, - Image = 0.00000025m, - Audio = 0.0000005m, - InputAudioCache = 0.00000005m, - InputCacheRead = 0.000000025m, - InputCacheWrite = 0.00000008333333333333334m, - InternalReasoning = 0.0000015m, + Prompt = 0.000000375m, + Completion = 0.000001875m, + Image = 0.000000375m, + Audio = 0.000000375m, + InputAudioCache = 0.0000000375m, + InputCacheRead = 0.0000000375m, + InputCacheWrite = 0.0000000208333333333333m, + InternalReasoning = 0.000001875m, + WebSearch = 0.014m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000001875m, + Completion = 0.0000009375m, + Image = 0.0000001875m, + Audio = 0.0000001875m, + InputAudioCache = 0.00000001875m, + InputCacheRead = 0.00000001875m, + InputCacheWrite = 0.0000000208333333333333m, + InternalReasoning = 0.0000009375m, WebSearch = 0.014m, }, + Aliases = new List { "google/gemini-3.7-flash:batch" }, }, new AIModelCapabilities @@ -543,8 +773,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "kwaipilot/kat-coder-pro-v2.5", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsBatch = false, Verified = false, - Rank = 9850, + Rank = 9795, ContextLimit = 256000, Created = new DateTime(2026, 7, 10), Pricing = new AIModelPricing @@ -555,13 +786,74 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "qwen/qwen3.8-27b", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9790, + ContextLimit = 1000000, + Created = new DateTime(2026, 8, 14), + Pricing = new AIModelPricing + { + Prompt = 0.0000004m, + Completion = 0.000003m, + InputCacheRead = 0.00000005m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "bytedance-seed/seed-2.0-code", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9785, + ContextLimit = 262144, + Created = new DateTime(2026, 8, 12), + Pricing = new AIModelPricing + { + Prompt = 0.0000005m, + Completion = 0.000003m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "z-ai/glm-5.2", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, + Verified = false, + Rank = 9780, + ContextLimit = 1048576, + Created = new DateTime(2026, 6, 16), + Pricing = new AIModelPricing + { + Prompt = 0.000000966m, + Completion = 0.000003036m, + InputCacheRead = 0.0000001932m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000014m, + Completion = 0.0000044m, + InputCacheRead = 0.00000026m, + }, + Aliases = new List { "z-ai/glm-5.2:batch" }, + }, + new AIModelCapabilities { Provider = provider, Model = "google/gemini-3.5-flash-lite", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9845, + Rank = 9775, ContextLimit = 1048576, Created = new DateTime(2026, 7, 21), Pricing = new AIModelPricing @@ -572,10 +864,40 @@ public override Task> RetrieveModels() Audio = 0.0000003m, InputAudioCache = 0.00000003m, InputCacheRead = 0.00000003m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000025m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000015m, + Completion = 0.00000125m, + Image = 0.00000015m, + Audio = 0.00000015m, + InputAudioCache = 0.000000015m, + InputCacheRead = 0.000000015m, + InternalReasoning = 0.00000125m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-3.5-flash-lite:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "deepseek/deepseek-v4-pro-0813", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9770, + ContextLimit = 1048576, + Created = new DateTime(2026, 8, 12), + Pricing = new AIModelPricing + { + Prompt = 0.000001122m, + Completion = 0.000003366m, + InputCacheRead = 0.0000000374m, + }, }, new AIModelCapabilities @@ -583,16 +905,24 @@ public override Task> RetrieveModels() Provider = provider, Model = "moonshotai/kimi-k2.7-code", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9840, + Rank = 9765, ContextLimit = 262144, Created = new DateTime(2026, 6, 12), Pricing = new AIModelPricing { - Prompt = 0.00000073m, - Completion = 0.0000035m, - InputCacheRead = 0.00000015m, + Prompt = 0.00000067m, + Completion = 0.0000034m, + InputCacheRead = 0.00000017m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000095m, + Completion = 0.000004m, + InputCacheRead = 0.00000019m, + }, + Aliases = new List { "moonshotai/kimi-k2.7-code:batch" }, }, new AIModelCapabilities @@ -600,8 +930,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "nvidia/nemotron-3-ultra-550b-a55b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9835, + Rank = 9760, ContextLimit = 512288, Created = new DateTime(2026, 6, 4), Pricing = new AIModelPricing @@ -610,6 +941,32 @@ public override Task> RetrieveModels() Completion = 0.0000036m, InputCacheRead = 0.0000002m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000006m, + Completion = 0.0000036m, + InputCacheRead = 0.0000002m, + }, + Aliases = new List { "nvidia/nemotron-3-ultra-550b-a55b:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "sakana/sakana-namazu", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9755, + ContextLimit = 262144, + Created = new DateTime(2026, 8, 11), + Pricing = new AIModelPricing + { + Prompt = 0.00000095m, + Completion = 0.000004m, + InputCacheRead = 0.00000015m, + WebSearch = 0.007m, + }, }, new AIModelCapabilities @@ -617,8 +974,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "thinkingmachines/inkling", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9830, + Rank = 9750, ContextLimit = 1048576, Created = new DateTime(2026, 7, 17), Pricing = new AIModelPricing @@ -627,6 +985,32 @@ public override Task> RetrieveModels() Completion = 0.00000405m, InputCacheRead = 0.00000017m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.00000405m, + InputCacheRead = 0.00000017m, + }, + Aliases = new List { "thinkingmachines/inkling:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "meta/muse-spark-1.2", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9745, + ContextLimit = 1048576, + Created = new DateTime(2026, 8, 5), + Pricing = new AIModelPricing + { + Prompt = 0.00000125m, + Completion = 0.00000425m, + InputCacheRead = 0.00000015m, + WebSearch = 0.0025m, + }, }, new AIModelCapabilities @@ -634,8 +1018,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "meta/muse-spark-1.1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9825, + Rank = 9740, ContextLimit = 1048576, Created = new DateTime(2026, 7, 16), Pricing = new AIModelPricing @@ -650,66 +1035,140 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3.7-max", + Model = "~z-ai/glm-latest", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9820, - ContextLimit = 1000000, - Created = new DateTime(2026, 5, 21), + Rank = 9735, + ContextLimit = 1048576, + Created = new DateTime(2026, 8, 19), Pricing = new AIModelPricing { - Prompt = 0.000001475m, - Completion = 0.000004425m, - InputCacheRead = 0.000000295m, - InputCacheWrite = 0.00000184375m, + Prompt = 0.0000014m, + Completion = 0.0000044m, + InputCacheRead = 0.00000026m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.6-terra-pro", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "z-ai/glm-5.3", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9815, - ContextLimit = 1050000, - Created = new DateTime(2026, 7, 9), + Rank = 9730, + ContextLimit = 1048576, + Created = new DateTime(2026, 8, 18), Pricing = new AIModelPricing { - Prompt = 0.00000100000000000000015m, + Prompt = 0.0000014m, + Completion = 0.0000044m, + InputCacheRead = 0.00000026m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "google/gemini-3.6-flash", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, + Verified = false, + Rank = 9725, + ContextLimit = 1048576, + Created = new DateTime(2026, 7, 21), + Pricing = new AIModelPricing + { + Prompt = 0.00000075m, + Completion = 0.00000375m, + Image = 0.00000075m, + Audio = 0.00000075m, + InputAudioCache = 0.000000075m, + InputCacheRead = 0.000000075m, + InputCacheWrite = 0.0000000416666666666667m, + InternalReasoning = 0.00000375m, + WebSearch = 0.014m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000375m, + Completion = 0.000001875m, + Image = 0.000000375m, + Audio = 0.000000375m, + InputAudioCache = 0.0000000375m, + InputCacheRead = 0.0000000375m, + InputCacheWrite = 0.0000000416666666666667m, + InternalReasoning = 0.000001875m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-3.6-flash:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "qwen/qwen3.8-2.4t-a95b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9720, + ContextLimit = 1048576, + Created = new DateTime(2026, 8, 12), + Pricing = new AIModelPricing + { + Prompt = 0.000002m, Completion = 0.000006m, - InputCacheRead = 0.0000001m, - InputCacheWrite = 0.00000125m, - WebSearch = 0.005m, + InputCacheRead = 0.00000025m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.6-terra", + Model = "x-ai/grok-4.6", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9810, - ContextLimit = 1050000, - Created = new DateTime(2026, 7, 9), + Rank = 9715, + ContextLimit = 500000, + Created = new DateTime(2026, 8, 12), Pricing = new AIModelPricing { - Prompt = 0.00000100000000000000015m, + Prompt = 0.000002m, Completion = 0.000006m, - InputCacheRead = 0.0000001m, - InputCacheWrite = 0.00000125m, + InputCacheRead = 0.0000005m, WebSearch = 0.005m, }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "qwen/qwen3.8-max", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9710, + ContextLimit = 1000000, + Created = new DateTime(2026, 8, 3), + Pricing = new AIModelPricing + { + Prompt = 0.000002m, + Completion = 0.000006m, + InputCacheRead = 0.00000025m, + InputCacheWrite = 0.0000025m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "x-ai/grok-4.5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9805, + Rank = 9705, ContextLimit = 500000, Created = new DateTime(2026, 7, 8), Pricing = new AIModelPricing @@ -726,15 +1185,16 @@ public override Task> RetrieveModels() Provider = provider, Model = "~x-ai/grok-latest", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9800, + Rank = 9700, ContextLimit = 500000, Created = new DateTime(2026, 7, 8), Pricing = new AIModelPricing { Prompt = 0.000002m, Completion = 0.000006m, - InputCacheRead = 0.0000003m, + InputCacheRead = 0.0000005m, WebSearch = 0.005m, }, }, @@ -744,8 +1204,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "aion-labs/aion-3.0", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9795, + Rank = 9695, ContextLimit = 131072, Created = new DateTime(2026, 7, 7), Pricing = new AIModelPricing @@ -759,12 +1220,13 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "anthropic/claude-sonnet-5", + Model = "openai/gpt-5.6-sol-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9790, - ContextLimit = 1000000, - Created = new DateTime(2026, 6, 30), + Rank = 9690, + ContextLimit = 1050000, + Created = new DateTime(2026, 7, 9), Pricing = new AIModelPricing { Prompt = 0.000002m, @@ -773,145 +1235,205 @@ public override Task> RetrieveModels() InputCacheWrite = 0.0000025m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000005m, + InputCacheRead = 0.0000001m, + InputCacheWrite = 0.00000125m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.6-sol-pro:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "google/gemini-3.6-flash", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "openai/gpt-5.6-sol", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9785, - ContextLimit = 1048576, - Created = new DateTime(2026, 7, 21), + Rank = 9685, + ContextLimit = 1050000, + Created = new DateTime(2026, 7, 9), Pricing = new AIModelPricing { - Prompt = 0.0000015m, - Completion = 0.0000075m, - Image = 0.0000015m, - Audio = 0.0000015m, - InputAudioCache = 0.00000015m, - InputCacheRead = 0.00000015m, - InputCacheWrite = 0.00000008333333333333334m, - InternalReasoning = 0.0000075m, - WebSearch = 0.014m, + Prompt = 0.000002m, + Completion = 0.00001m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.0000025m, + WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000005m, + InputCacheRead = 0.0000001m, + InputCacheWrite = 0.00000125m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.6-sol:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "google/gemini-3.5-flash", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "anthropic/claude-sonnet-5", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9780, - ContextLimit = 1048576, - Created = new DateTime(2026, 5, 19), + Rank = 9680, + ContextLimit = 1000000, + Created = new DateTime(2026, 6, 30), Pricing = new AIModelPricing { - Prompt = 0.0000015m, - Completion = 0.000009m, - Image = 0.0000015m, - Audio = 0.000003m, - InputAudioCache = 0.0000003m, - InputCacheRead = 0.00000015m, - InputCacheWrite = 0.00000008333333333333334m, - InternalReasoning = 0.000009m, - WebSearch = 0.014m, + Prompt = 0.000002m, + Completion = 0.00001m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.0000025m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000005m, + InputCacheRead = 0.0000001m, + InputCacheWrite = 0.00000125m, + WebSearch = 0.01m, }, + Aliases = new List { "anthropic/claude-sonnet-5:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "moonshotai/kimi-k3", + Model = "openai/gpt-5.6-terra-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9775, - ContextLimit = 1048576, - Created = new DateTime(2026, 7, 16), + Rank = 9675, + ContextLimit = 1050000, + Created = new DateTime(2026, 7, 9), Pricing = new AIModelPricing { - Prompt = 0.000003m, - Completion = 0.000015m, - InputCacheRead = 0.0000003m, + Prompt = 0.000002m, + Completion = 0.000012m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.0000025m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000006m, + InputCacheRead = 0.0000001m, + WebSearch = 0.01m, }, + Aliases = new List { "openai/gpt-5.6-terra-pro:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "anthropic/claude-opus-5", + Model = "openai/gpt-5.6-terra", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9770, - ContextLimit = 1000000, - Created = new DateTime(2026, 7, 24), + Rank = 9670, + ContextLimit = 1050000, + Created = new DateTime(2026, 7, 9), Pricing = new AIModelPricing { - Prompt = 0.000005m, - Completion = 0.000025m, - InputCacheRead = 0.0000005m, - InputCacheWrite = 0.00000625m, + Prompt = 0.000002m, + Completion = 0.000012m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.0000025m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000006m, + InputCacheRead = 0.0000001m, WebSearch = 0.01m, }, + Aliases = new List { "openai/gpt-5.6-terra:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "anthropic/claude-opus-4.8", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "moonshotai/kimi-k3", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9765, - ContextLimit = 1000000, - Created = new DateTime(2026, 5, 27), - Pricing = new AIModelPricing - { - Prompt = 0.000005m, - Completion = 0.000025m, - InputCacheRead = 0.0000005m, - InputCacheWrite = 0.00000625m, - WebSearch = 0.01m, + Rank = 9665, + ContextLimit = 1048576, + Created = new DateTime(2026, 7, 16), + Pricing = new AIModelPricing + { + Prompt = 0.000003m, + Completion = 0.000015m, + InputCacheRead = 0.0000003m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.6-sol-pro", + Model = "anthropic/claude-opus-5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9760, - ContextLimit = 1050000, - Created = new DateTime(2026, 7, 9), + Rank = 9660, + ContextLimit = 1000000, + Created = new DateTime(2026, 7, 24), Pricing = new AIModelPricing { Prompt = 0.000005m, - Completion = 0.00003m, + Completion = 0.000025m, InputCacheRead = 0.0000005m, InputCacheWrite = 0.00000625m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.0000125m, + InputCacheRead = 0.00000025m, + InputCacheWrite = 0.000003125m, + WebSearch = 0.01m, + }, + Aliases = new List { "anthropic/claude-opus-5:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.6-sol", + Model = "anthropic/claude-opus-4.8", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9755, - ContextLimit = 1050000, - Created = new DateTime(2026, 7, 9), + Rank = 9655, + ContextLimit = 1000000, + Created = new DateTime(2026, 5, 27), Pricing = new AIModelPricing { Prompt = 0.000005m, - Completion = 0.00003m, + Completion = 0.000025m, InputCacheRead = 0.0000005m, InputCacheWrite = 0.00000625m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.0000125m, + InputCacheRead = 0.00000025m, + InputCacheWrite = 0.000003125m, + WebSearch = 0.01m, + }, + Aliases = new List { "anthropic/claude-opus-4.8:batch" }, }, new AIModelCapabilities @@ -919,8 +1441,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "sakana/fugu-ultra", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9750, + Rank = 9650, ContextLimit = 1000000, Created = new DateTime(2026, 6, 24), Pricing = new AIModelPricing @@ -932,32 +1455,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "openai/gpt-chat-latest", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - Verified = false, - Rank = 9745, - ContextLimit = 400000, - Created = new DateTime(2026, 5, 5), - Pricing = new AIModelPricing - { - Prompt = 0.000005m, - Completion = 0.00003m, - InputCacheRead = 0.0000005m, - WebSearch = 0.01m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "google/gemini-3.1-flash-lite-image", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, Default = AICapability.Text2Image | AICapability.Image2Image | AICapability.Image2Text, + SupportsBatch = false, Verified = false, - Rank = 9740, + Rank = 9645, ContextLimit = 65536, Created = new DateTime(2026, 6, 30), Pricing = new AIModelPricing @@ -974,8 +1480,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "anthropic/claude-opus-5-fast", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9735, + Rank = 9640, ContextLimit = 1000000, Created = new DateTime(2026, 7, 24), Pricing = new AIModelPricing @@ -993,8 +1500,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "~anthropic/claude-fable-latest", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9730, + Rank = 9635, ContextLimit = 1000000, Created = new DateTime(2026, 6, 9), Pricing = new AIModelPricing @@ -1012,8 +1520,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "anthropic/claude-fable-5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, Verified = false, - Rank = 9725, + Rank = 9630, ContextLimit = 1000000, Created = new DateTime(2026, 6, 9), Pricing = new AIModelPricing @@ -1024,6 +1533,15 @@ public override Task> RetrieveModels() InputCacheWrite = 0.0000125m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000005m, + Completion = 0.000025m, + InputCacheRead = 0.0000005m, + InputCacheWrite = 0.00000625m, + WebSearch = 0.01m, + }, + Aliases = new List { "anthropic/claude-fable-5:batch" }, }, new AIModelCapabilities @@ -1031,8 +1549,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "anthropic/claude-opus-4.8-fast", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9720, + Rank = 9625, ContextLimit = 1000000, Created = new DateTime(2026, 5, 27), Pricing = new AIModelPricing @@ -1050,8 +1569,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "google/gemini-3.1-flash-image", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9715, + Rank = 9620, ContextLimit = 131072, Created = new DateTime(2026, 6, 18), Pricing = new AIModelPricing @@ -1068,8 +1588,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "google/gemini-3-pro-image", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9710, + Rank = 9615, ContextLimit = 131072, Created = new DateTime(2026, 6, 18), Pricing = new AIModelPricing @@ -1087,25 +1608,6 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "anthropic/claude-opus-4.7-fast", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - Verified = false, - Rank = 9705, - ContextLimit = 1000000, - Created = new DateTime(2026, 5, 12), - Pricing = new AIModelPricing - { - Prompt = 0.00003m, - Completion = 0.00015m, - InputCacheRead = 0.000003m, - InputCacheWrite = 0.0000375m, - WebSearch = 0.01m, - }, - }, - // Released between February 2026 and May 2026 @@ -1116,8 +1618,9 @@ public override Task> RetrieveModels() Model = "openrouter/pareto-code", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9700, + Rank = 9610, ContextLimit = 2000000, Created = new DateTime(2026, 4, 21), Pricing = new AIModelPricing @@ -1133,8 +1636,9 @@ public override Task> RetrieveModels() Model = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9695, + Rank = 9605, ContextLimit = 256000, Created = new DateTime(2026, 4, 28), }, @@ -1145,8 +1649,9 @@ public override Task> RetrieveModels() Model = "google/gemma-4-26b-a4b-it:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9690, + Rank = 9600, ContextLimit = 262144, Created = new DateTime(2026, 4, 3), }, @@ -1157,8 +1662,9 @@ public override Task> RetrieveModels() Model = "google/gemma-4-31b-it:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9685, + Rank = 9595, ContextLimit = 262144, Created = new DateTime(2026, 4, 2), }, @@ -1170,8 +1676,9 @@ public override Task> RetrieveModels() Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.JsonOutput, Default = AICapability.Text2Speech, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9680, + Rank = 9590, ContextLimit = 1048576, Created = new DateTime(2026, 3, 30), }, @@ -1182,8 +1689,9 @@ public override Task> RetrieveModels() Model = "google/lyria-3-clip-preview", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9675, + Rank = 9585, ContextLimit = 1048576, Created = new DateTime(2026, 3, 30), }, @@ -1194,38 +1702,22 @@ public override Task> RetrieveModels() Model = "nvidia/nemotron-3-super-120b-a12b:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9670, + Rank = 9580, ContextLimit = 262144, Created = new DateTime(2026, 3, 11), }, - new AIModelCapabilities - { - Provider = provider, - Model = "inclusionai/ling-2.6-flash", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 9665, - ContextLimit = 262144, - Created = new DateTime(2026, 4, 21), - Pricing = new AIModelPricing - { - Prompt = 0.00000001m, - Completion = 0.00000003m, - InputCacheRead = 0.000000002m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "ibm-granite/granite-4.1-8b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9660, + Rank = 9575, ContextLimit = 131072, Created = new DateTime(2026, 4, 30), Pricing = new AIModelPricing @@ -1242,8 +1734,9 @@ public override Task> RetrieveModels() Model = "rekaai/reka-edge", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9655, + Rank = 9570, ContextLimit = 16384, Created = new DateTime(2026, 3, 20), Pricing = new AIModelPricing @@ -1256,34 +1749,37 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3.5-9b", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "deepseek/deepseek-v4-flash", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9650, - ContextLimit = 262144, - Created = new DateTime(2026, 3, 10), + Rank = 9565, + ContextLimit = 1048576, + Created = new DateTime(2026, 4, 24), Pricing = new AIModelPricing { - Prompt = 0.0000001m, - Completion = 0.00000015m, + Prompt = 0.00000005306m, + Completion = 0.00000010612m, + InputCacheRead = 0.000000010612m, }, }, new AIModelCapabilities { Provider = provider, - Model = "tencent/hy3-preview", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + Model = "qwen/qwen3.5-9b", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9645, + Rank = 9560, ContextLimit = 262144, - Created = new DateTime(2026, 4, 22), + Created = new DateTime(2026, 3, 10), Pricing = new AIModelPricing { - Prompt = 0.000000063m, - Completion = 0.00000021m, - InputCacheRead = 0.000000021m, + Prompt = 0.0000001m, + Completion = 0.00000015m, }, }, @@ -1293,8 +1789,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3.5-flash-02-23", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9640, + Rank = 9555, ContextLimit = 1000000, Created = new DateTime(2026, 2, 25), Pricing = new AIModelPricing @@ -1305,32 +1802,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "deepseek/deepseek-v4-flash", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9635, - ContextLimit = 1048576, - Created = new DateTime(2026, 4, 24), - Pricing = new AIModelPricing - { - Prompt = 0.00000014m, - Completion = 0.00000028m, - InputCacheRead = 0.000000028m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "xiaomi/mimo-v2.5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9630, + Rank = 9550, ContextLimit = 1050000, Created = new DateTime(2026, 4, 22), Pricing = new AIModelPricing @@ -1347,8 +1827,9 @@ public override Task> RetrieveModels() Model = "google/gemma-4-26b-a4b-it", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9625, + Rank = 9545, ContextLimit = 262144, Created = new DateTime(2026, 4, 3), Pricing = new AIModelPricing @@ -1365,8 +1846,9 @@ public override Task> RetrieveModels() Model = "google/gemma-4-31b-it", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9620, + Rank = 9540, ContextLimit = 262144, Created = new DateTime(2026, 4, 2), Pricing = new AIModelPricing @@ -1383,8 +1865,9 @@ public override Task> RetrieveModels() Model = "nvidia/nemotron-3-super-120b-a12b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9615, + Rank = 9535, ContextLimit = 1000000, Created = new DateTime(2026, 3, 11), Pricing = new AIModelPricing @@ -1401,102 +1884,32 @@ public override Task> RetrieveModels() Model = "bytedance-seed/seed-2.0-mini", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9610, + Rank = 9530, ContextLimit = 262144, Created = new DateTime(2026, 2, 26), Pricing = new AIModelPricing { - Prompt = 0.0000001m, - Completion = 0.0000004m, - }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "mistralai/mistral-small-2603", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9605, - ContextLimit = 262144, - Created = new DateTime(2026, 3, 16), - Pricing = new AIModelPricing - { - Prompt = 0.00000015m, - Completion = 0.0000006m, - InputCacheRead = 0.000000015m, - }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "inclusionai/ling-2.6-1t", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - Verified = false, - Rank = 9600, - ContextLimit = 262144, - Created = new DateTime(2026, 4, 23), - Pricing = new AIModelPricing - { - Prompt = 0.000000075m, - Completion = 0.000000625m, - InputCacheRead = 0.000000015m, - }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "inception/mercury-2", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9595, - ContextLimit = 128000, - Created = new DateTime(2026, 3, 4), - Pricing = new AIModelPricing - { - Prompt = 0.00000025m, - Completion = 0.00000075m, - InputCacheRead = 0.000000025m, - }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "qwen/qwen3-coder-next", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 9590, - ContextLimit = 262144, - Created = new DateTime(2026, 2, 4), - Pricing = new AIModelPricing - { - Prompt = 0.00000012m, - Completion = 0.0000008m, - InputCacheRead = 0.00000007m, + Prompt = 0.0000001m, + Completion = 0.0000004m, }, }, new AIModelCapabilities { Provider = provider, - Model = "arcee-ai/trinity-large-thinking", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, + Model = "tencent/hy3-preview", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9585, + Rank = 9525, ContextLimit = 262144, - Created = new DateTime(2026, 4, 1), + Created = new DateTime(2026, 4, 22), Pricing = new AIModelPricing { - Prompt = 0.00000022m, - Completion = 0.00000085m, + Prompt = 0.00000018m, + Completion = 0.0000006m, InputCacheRead = 0.00000006m, }, }, @@ -1504,103 +1917,109 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "deepseek/deepseek-v4-pro", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "mistralai/mistral-small-2603", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9580, - ContextLimit = 1048576, - Created = new DateTime(2026, 4, 24), + Rank = 9520, + ContextLimit = 262144, + Created = new DateTime(2026, 3, 16), Pricing = new AIModelPricing { - Prompt = 0.000000435m, - Completion = 0.00000087m, - InputCacheRead = 0.000000003625m, + Prompt = 0.00000015m, + Completion = 0.0000006m, + InputCacheRead = 0.000000015m, }, }, new AIModelCapabilities { Provider = provider, - Model = "xiaomi/mimo-v2.5-pro", + Model = "inception/mercury-2", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9575, - ContextLimit = 1050000, - Created = new DateTime(2026, 4, 22), + Rank = 9515, + ContextLimit = 128000, + Created = new DateTime(2026, 3, 4), Pricing = new AIModelPricing { - Prompt = 0.000000435m, - Completion = 0.00000087m, - InputCacheRead = 0.0000000036m, + Prompt = 0.00000025m, + Completion = 0.00000075m, + InputCacheRead = 0.000000025m, }, }, new AIModelCapabilities { Provider = provider, - Model = "minimax/minimax-m2.5", + Model = "deepseek/deepseek-v4-pro", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9570, - ContextLimit = 204800, - Created = new DateTime(2026, 2, 12), + Rank = 9510, + ContextLimit = 1048576, + Created = new DateTime(2026, 4, 24), Pricing = new AIModelPricing { - Prompt = 0.00000015m, - Completion = 0.0000009m, - InputCacheRead = 0.00000005m, + Prompt = 0.000000396894m, + Completion = 0.000000793788m, + InputCacheRead = 0.0000000330745m, }, }, new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3.6-35b-a3b", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "arcee-ai/trinity-large-thinking", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9565, + Rank = 9505, ContextLimit = 262144, - Created = new DateTime(2026, 4, 27), + Created = new DateTime(2026, 4, 1), Pricing = new AIModelPricing { - Prompt = 0.00000014m, - Completion = 0.000001m, - InputCacheRead = 0.00000005m, + Prompt = 0.00000022m, + Completion = 0.00000085m, + InputCacheRead = 0.00000006m, }, }, new AIModelCapabilities { Provider = provider, - Model = "minimax/minimax-m2.7", + Model = "xiaomi/mimo-v2.5-pro", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9560, - ContextLimit = 204800, - Created = new DateTime(2026, 3, 18), + Rank = 9500, + ContextLimit = 1050000, + Created = new DateTime(2026, 4, 22), Pricing = new AIModelPricing { - Prompt = 0.00000025m, - Completion = 0.000001m, - InputCacheRead = 0.00000005m, + Prompt = 0.000000435m, + Completion = 0.00000087m, + InputCacheRead = 0.0000000036m, }, }, new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3.5-35b-a3b", + Model = "qwen/qwen3.6-35b-a3b", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9555, + Rank = 9495, ContextLimit = 262144, - Created = new DateTime(2026, 2, 25), + Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing { Prompt = 0.00000014m, @@ -1615,8 +2034,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3.6-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9550, + Rank = 9490, ContextLimit = 1000000, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing @@ -1633,8 +2053,9 @@ public override Task> RetrieveModels() Model = "kwaipilot/kat-coder-pro-v2", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9545, + Rank = 9485, ContextLimit = 262144, Created = new DateTime(2026, 3, 27), Pricing = new AIModelPricing @@ -1645,14 +2066,34 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "minimax/minimax-m2.7", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 9480, + ContextLimit = 204800, + Created = new DateTime(2026, 3, 18), + Pricing = new AIModelPricing + { + Prompt = 0.0000003m, + Completion = 0.0000012m, + InputCacheRead = 0.00000006m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "openai/gpt-5.4-nano", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9540, + Rank = 9475, ContextLimit = 400000, Created = new DateTime(2026, 3, 17), Pricing = new AIModelPricing @@ -1662,40 +2103,67 @@ public override Task> RetrieveModels() InputCacheRead = 0.00000002m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000001m, + Completion = 0.000000625m, + InputCacheRead = 0.00000001m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.4-nano:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3.5-27b", + Model = "qwen/qwen3.5-35b-a3b", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9535, + Rank = 9470, ContextLimit = 262144, Created = new DateTime(2026, 2, 25), Pricing = new AIModelPricing { - Prompt = 0.000000195m, - Completion = 0.00000156m, + Prompt = 0.00000025m, + Completion = 0.00000125m, + InputCacheRead = 0.00000025m, }, }, new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3.5-plus-02-15", + Model = "perceptron/perceptron-mk1", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9465, + ContextLimit = 32768, + Created = new DateTime(2026, 5, 12), + Pricing = new AIModelPricing + { + Prompt = 0.00000015m, + Completion = 0.0000015m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "qwen/qwen3.5-27b", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9530, - ContextLimit = 1000000, - Created = new DateTime(2026, 2, 16), + Rank = 9460, + ContextLimit = 262144, + Created = new DateTime(2026, 2, 25), Pricing = new AIModelPricing { - Prompt = 0.00000026m, + Prompt = 0.000000195m, Completion = 0.00000156m, - InputCacheWrite = 0.000000325m, }, }, @@ -1705,8 +2173,9 @@ public override Task> RetrieveModels() Model = "aion-labs/aion-2.0", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9525, + Rank = 9455, ContextLimit = 131072, Created = new DateTime(2026, 2, 23), Pricing = new AIModelPricing @@ -1723,8 +2192,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3.5-plus-20260420", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9520, + Rank = 9450, ContextLimit = 1000000, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing @@ -1741,8 +2211,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3.6-plus", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9515, + Rank = 9445, ContextLimit = 1000000, Created = new DateTime(2026, 4, 2), Pricing = new AIModelPricing @@ -1756,18 +2227,19 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3.6-27b", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, + Model = "x-ai/grok-build-0.1", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9510, - ContextLimit = 262144, - Created = new DateTime(2026, 4, 27), + Rank = 9440, + ContextLimit = 256000, + Created = new DateTime(2026, 5, 20), Pricing = new AIModelPricing { - Prompt = 0.0000003m, + Prompt = 0.000001m, Completion = 0.000002m, - InputCacheRead = 0.00000015m, + InputCacheRead = 0.0000002m, + WebSearch = 0.005m, }, }, @@ -1777,8 +2249,9 @@ public override Task> RetrieveModels() Model = "bytedance-seed/seed-2.0-lite", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9505, + Rank = 9435, ContextLimit = 262144, Created = new DateTime(2026, 3, 10), Pricing = new AIModelPricing @@ -1794,8 +2267,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3.5-122b-a10b", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9500, + Rank = 9430, ContextLimit = 262144, Created = new DateTime(2026, 2, 25), Pricing = new AIModelPricing @@ -1808,13 +2282,14 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "google/gemini-3.1-flash-lite-preview", + Model = "google/gemini-3.1-flash-lite", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, + Default = AICapability.Speech2Text, + SupportsBatch = true, Verified = false, - Rank = 9495, + Rank = 9425, ContextLimit = 1048576, - Created = new DateTime(2026, 3, 3), + Created = new DateTime(2026, 5, 7), Pricing = new AIModelPricing { Prompt = 0.00000025m, @@ -1823,27 +2298,46 @@ public override Task> RetrieveModels() Audio = 0.0000005m, InputAudioCache = 0.00000005m, InputCacheRead = 0.000000025m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000015m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000125m, + Completion = 0.00000075m, + Image = 0.000000125m, + Audio = 0.00000025m, + InputAudioCache = 0.000000025m, + InputCacheRead = 0.0000000125m, + InternalReasoning = 0.00000075m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-3.1-flash-lite:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3.5-397b-a17b", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "google/gemini-3.1-flash-lite-preview", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9490, - ContextLimit = 262144, - Created = new DateTime(2026, 2, 16), + Rank = 9420, + ContextLimit = 1048576, + Created = new DateTime(2026, 3, 3), Pricing = new AIModelPricing { - Prompt = 0.00000039m, - Completion = 0.00000234m, - InputCacheRead = 0.000000111m, + Prompt = 0.00000025m, + Completion = 0.0000015m, + Image = 0.00000025m, + Audio = 0.0000005m, + InputAudioCache = 0.00000005m, + InputCacheRead = 0.000000025m, + InputCacheWrite = 0.0000000833333333333333m, + InternalReasoning = 0.0000015m, + WebSearch = 0.014m, }, }, @@ -1853,8 +2347,9 @@ public override Task> RetrieveModels() Model = "x-ai/grok-4.3", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9485, + Rank = 9415, ContextLimit = 1000000, Created = new DateTime(2026, 4, 30), Pricing = new AIModelPricing @@ -1872,8 +2367,9 @@ public override Task> RetrieveModels() Model = "x-ai/grok-4.20-multi-agent", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9480, + Rank = 9410, ContextLimit = 2000000, Created = new DateTime(2026, 3, 31), Pricing = new AIModelPricing @@ -1891,8 +2387,9 @@ public override Task> RetrieveModels() Model = "x-ai/grok-4.20", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9475, + Rank = 9405, ContextLimit = 2000000, Created = new DateTime(2026, 3, 31), Pricing = new AIModelPricing @@ -1907,18 +2404,25 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "z-ai/glm-5", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "~google/gemini-flash-latest", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9470, - ContextLimit = 204800, - Created = new DateTime(2026, 2, 11), + Rank = 9400, + ContextLimit = 1048576, + Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing { - Prompt = 0.00000095m, - Completion = 0.00000255m, - InputCacheRead = 0.0000002m, + Prompt = 0.000000375m, + Completion = 0.000001875m, + Image = 0.000000375m, + Audio = 0.000000375m, + InputAudioCache = 0.0000000375m, + InputCacheRead = 0.0000000375m, + InputCacheWrite = 0.0000000208333333333333m, + InternalReasoning = 0.000001875m, + WebSearch = 0.014m, }, }, @@ -1928,8 +2432,9 @@ public override Task> RetrieveModels() Model = "z-ai/glm-5.1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9465, + Rank = 9395, ContextLimit = 204800, Created = new DateTime(2026, 4, 7), Pricing = new AIModelPricing @@ -1940,56 +2445,79 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "qwen/qwen3.6-27b", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 9390, + ContextLimit = 262144, + Created = new DateTime(2026, 4, 27), + Pricing = new AIModelPricing + { + Prompt = 0.0000006m, + Completion = 0.0000036m, + InputCacheRead = 0.00000012m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "moonshotai/kimi-k2.6", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9460, + Rank = 9385, ContextLimit = 262144, Created = new DateTime(2026, 4, 20), Pricing = new AIModelPricing { - Prompt = 0.0000006m, - Completion = 0.00000341m, - InputCacheRead = 0.0000002m, + Prompt = 0.00000095m, + Completion = 0.000004m, + InputCacheRead = 0.00000016m, }, }, new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3-max-thinking", + Model = "z-ai/glm-5-turbo", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9455, - ContextLimit = 262144, - Created = new DateTime(2026, 2, 9), + Rank = 9380, + ContextLimit = 202752, + Created = new DateTime(2026, 3, 15), Pricing = new AIModelPricing { - Prompt = 0.00000078m, - Completion = 0.0000039m, + Prompt = 0.0000012m, + Completion = 0.000004m, + InputCacheRead = 0.00000024m, }, }, new AIModelCapabilities { Provider = provider, - Model = "z-ai/glm-5-turbo", + Model = "qwen/qwen3.7-max", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9450, - ContextLimit = 202752, - Created = new DateTime(2026, 3, 15), + Rank = 9375, + ContextLimit = 1000000, + Created = new DateTime(2026, 5, 21), Pricing = new AIModelPricing { - Prompt = 0.0000012m, - Completion = 0.000004m, - InputCacheRead = 0.00000024m, + Prompt = 0.000001475m, + Completion = 0.000004425m, + InputCacheRead = 0.000000295m, + InputCacheWrite = 0.00000184375m, }, }, @@ -1999,8 +2527,9 @@ public override Task> RetrieveModels() Model = "~openai/gpt-mini-latest", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9445, + Rank = 9370, ContextLimit = 400000, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing @@ -2018,8 +2547,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5.4-mini", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9440, + Rank = 9365, ContextLimit = 400000, Created = new DateTime(2026, 3, 17), Pricing = new AIModelPricing @@ -2029,6 +2559,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.000000075m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000375m, + Completion = 0.00000225m, + InputCacheRead = 0.0000000375m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.4-mini:batch" }, }, new AIModelCapabilities @@ -2037,8 +2575,9 @@ public override Task> RetrieveModels() Model = "~anthropic/claude-haiku-latest", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9435, + Rank = 9360, ContextLimit = 200000, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing @@ -2057,8 +2596,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3.6-max-preview", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9430, + Rank = 9355, ContextLimit = 262144, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing @@ -2074,8 +2614,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "mistralai/mistral-medium-3-5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, - Rank = 9425, + Rank = 9350, ContextLimit = 262144, Created = new DateTime(2026, 4, 30), Pricing = new AIModelPricing @@ -2091,8 +2632,9 @@ public override Task> RetrieveModels() Model = "~anthropic/claude-sonnet-latest", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9420, + Rank = 9345, ContextLimit = 1000000, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing @@ -2108,24 +2650,21 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "~google/gemini-flash-latest", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "~openai/gpt-latest", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9415, - ContextLimit = 1048576, + Rank = 9340, + ContextLimit = 1050000, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing { - Prompt = 0.0000015m, - Completion = 0.0000075m, - Image = 0.0000015m, - Audio = 0.0000015m, - InputAudioCache = 0.00000015m, - InputCacheRead = 0.00000015m, - InputCacheWrite = 0.00000008333333333333334m, - InternalReasoning = 0.0000075m, - WebSearch = 0.014m, + Prompt = 0.000002m, + Completion = 0.00001m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.0000025m, + WebSearch = 0.01m, }, }, @@ -2133,28 +2672,66 @@ public override Task> RetrieveModels() { Provider = provider, Model = "~moonshotai/kimi-latest", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9410, + Rank = 9335, ContextLimit = 1048576, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing { - Prompt = 0.0000029m, - Completion = 0.000014m, + Prompt = 0.0000026m, + Completion = 0.000013m, InputCacheRead = 0.00000029m, }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "google/gemini-3.5-flash", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = true, + Verified = false, + Rank = 9330, + ContextLimit = 1048576, + Created = new DateTime(2026, 5, 19), + Pricing = new AIModelPricing + { + Prompt = 0.0000015m, + Completion = 0.000009m, + Image = 0.0000015m, + Audio = 0.000003m, + InputAudioCache = 0.0000003m, + InputCacheRead = 0.00000015m, + InputCacheWrite = 0.0000000833333333333333m, + InternalReasoning = 0.000009m, + WebSearch = 0.014m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000075m, + Completion = 0.0000045m, + Image = 0.00000075m, + Audio = 0.0000015m, + InputAudioCache = 0.00000015m, + InputCacheRead = 0.000000075m, + InternalReasoning = 0.0000045m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-3.5-flash:batch" }, + }, + new AIModelCapabilities { Provider = provider, Model = "openai/gpt-5.3-codex", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9405, + Rank = 9325, ContextLimit = 400000, Created = new DateTime(2026, 2, 24), Pricing = new AIModelPricing @@ -2172,8 +2749,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5.4", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9400, + Rank = 9320, ContextLimit = 1050000, Created = new DateTime(2026, 3, 5), Pricing = new AIModelPricing @@ -2183,26 +2761,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.00000025m, WebSearch = 0.01m, }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "anthropic/claude-sonnet-4.6", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9395, - ContextLimit = 1000000, - Created = new DateTime(2026, 2, 17), - Pricing = new AIModelPricing + BatchPricing = new AIModelPricing { - Prompt = 0.000003m, - Completion = 0.000015m, - InputCacheRead = 0.0000003m, - InputCacheWrite = 0.00000375m, + Prompt = 0.00000125m, + Completion = 0.0000075m, + InputCacheRead = 0.000000125m, WebSearch = 0.01m, }, + Aliases = new List { "openai/gpt-5.4:batch" }, }, new AIModelCapabilities @@ -2211,8 +2777,9 @@ public override Task> RetrieveModels() Model = "~google/gemini-pro-latest", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9390, + Rank = 9315, ContextLimit = 1048576, Created = new DateTime(2026, 4, 27), Pricing = new AIModelPricing @@ -2235,8 +2802,9 @@ public override Task> RetrieveModels() Model = "google/gemini-3.1-pro-preview-customtools", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9385, + Rank = 9310, ContextLimit = 1048576, Created = new DateTime(2026, 2, 25), Pricing = new AIModelPricing @@ -2253,38 +2821,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "google/gemini-3.1-pro-preview", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9380, - ContextLimit = 1048576, - Created = new DateTime(2026, 2, 19), - Pricing = new AIModelPricing - { - Prompt = 0.000002m, - Completion = 0.000012m, - Image = 0.000002m, - Audio = 0.000002m, - InputAudioCache = 0.0000002m, - InputCacheRead = 0.0000002m, - InputCacheWrite = 0.000000375m, - InternalReasoning = 0.000012m, - WebSearch = 0.014m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "~anthropic/claude-opus-latest", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9375, + Rank = 9305, ContextLimit = 1000000, Created = new DateTime(2026, 4, 21), Pricing = new AIModelPricing @@ -2303,8 +2848,9 @@ public override Task> RetrieveModels() Model = "anthropic/claude-opus-4.7", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9370, + Rank = 9300, ContextLimit = 1000000, Created = new DateTime(2026, 4, 16), Pricing = new AIModelPricing @@ -2315,44 +2861,32 @@ public override Task> RetrieveModels() InputCacheWrite = 0.00000625m, WebSearch = 0.01m, }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "anthropic/claude-opus-4.6", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9365, - ContextLimit = 1000000, - Created = new DateTime(2026, 2, 4), - Pricing = new AIModelPricing + BatchPricing = new AIModelPricing { - Prompt = 0.000005m, - Completion = 0.000025m, - InputCacheRead = 0.0000005m, - InputCacheWrite = 0.00000625m, + Prompt = 0.0000025m, + Completion = 0.0000125m, + InputCacheRead = 0.00000025m, + InputCacheWrite = 0.000003125m, WebSearch = 0.01m, }, + Aliases = new List { "anthropic/claude-opus-4.7:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "~openai/gpt-latest", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, + Model = "openai/gpt-chat-latest", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsBatch = false, Verified = false, - Rank = 9360, - ContextLimit = 1050000, - Created = new DateTime(2026, 4, 27), + Rank = 9295, + ContextLimit = 400000, + Created = new DateTime(2026, 5, 5), Pricing = new AIModelPricing { Prompt = 0.000005m, Completion = 0.00003m, InputCacheRead = 0.0000005m, - InputCacheWrite = 0.00000625m, WebSearch = 0.01m, }, }, @@ -2363,8 +2897,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5.5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9355, + Rank = 9290, ContextLimit = 1050000, Created = new DateTime(2026, 4, 24), Pricing = new AIModelPricing @@ -2374,6 +2909,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.0000005m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.000015m, + InputCacheRead = 0.00000025m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.5:batch" }, }, new AIModelCapabilities @@ -2382,8 +2925,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5.4-image-2", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9350, + Rank = 9285, ContextLimit = 272000, Created = new DateTime(2026, 4, 21), Pricing = new AIModelPricing @@ -2402,8 +2946,9 @@ public override Task> RetrieveModels() Model = "google/gemini-3.1-flash-image-preview", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9345, + Rank = 9280, ContextLimit = 65536, Created = new DateTime(2026, 2, 26), Pricing = new AIModelPricing @@ -2415,14 +2960,35 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "anthropic/claude-opus-4.7-fast", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Rank = 9275, + ContextLimit = 1000000, + Created = new DateTime(2026, 5, 12), + Pricing = new AIModelPricing + { + Prompt = 0.00003m, + Completion = 0.00015m, + InputCacheRead = 0.000003m, + InputCacheWrite = 0.0000375m, + WebSearch = 0.01m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "openai/gpt-5.5-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9340, + Rank = 9270, ContextLimit = 1050000, Created = new DateTime(2026, 4, 24), Pricing = new AIModelPricing @@ -2431,6 +2997,13 @@ public override Task> RetrieveModels() Completion = 0.00018m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000015m, + Completion = 0.00009m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.5-pro:batch" }, }, new AIModelCapabilities @@ -2439,8 +3012,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5.4-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9335, + Rank = 9265, ContextLimit = 1050000, Created = new DateTime(2026, 3, 5), Pricing = new AIModelPricing @@ -2449,6 +3023,13 @@ public override Task> RetrieveModels() Completion = 0.00018m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000015m, + Completion = 0.00009m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.4-pro:batch" }, }, @@ -2461,8 +3042,9 @@ public override Task> RetrieveModels() Model = "openrouter/bodybuilder", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9330, + Rank = 9260, ContextLimit = 128000, Created = new DateTime(2025, 12, 5), Pricing = new AIModelPricing @@ -2478,22 +3060,11 @@ public override Task> RetrieveModels() Model = "openrouter/free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9325, + Rank = 9255, ContextLimit = 200000, - Created = new DateTime(2026, 2, 1), - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "nvidia/nemotron-3-nano-30b-a3b:free", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9320, - ContextLimit = 256000, - Created = new DateTime(2025, 12, 14), + Created = new DateTime(2026, 2, 1), }, new AIModelCapabilities @@ -2502,8 +3073,9 @@ public override Task> RetrieveModels() Model = "mistralai/ministral-3b-2512", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9315, + Rank = 9250, ContextLimit = 131072, Created = new DateTime(2025, 12, 2), Pricing = new AIModelPricing @@ -2520,8 +3092,9 @@ public override Task> RetrieveModels() Model = "mistralai/ministral-8b-2512", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9310, + Rank = 9245, ContextLimit = 262144, Created = new DateTime(2025, 12, 2), Pricing = new AIModelPricing @@ -2538,8 +3111,9 @@ public override Task> RetrieveModels() Model = "nvidia/nemotron-3-nano-30b-a3b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9305, + Rank = 9240, ContextLimit = 262144, Created = new DateTime(2025, 12, 14), Pricing = new AIModelPricing @@ -2556,8 +3130,9 @@ public override Task> RetrieveModels() Model = "mistralai/ministral-14b-2512", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9300, + Rank = 9235, ContextLimit = 262144, Created = new DateTime(2025, 12, 2), Pricing = new AIModelPricing @@ -2574,8 +3149,9 @@ public override Task> RetrieveModels() Model = "stepfun/step-3.5-flash", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9295, + Rank = 9230, ContextLimit = 262144, Created = new DateTime(2026, 1, 29), Pricing = new AIModelPricing @@ -2592,8 +3168,9 @@ public override Task> RetrieveModels() Model = "bytedance-seed/seed-1.6-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9290, + Rank = 9225, ContextLimit = 262144, Created = new DateTime(2025, 12, 23), Pricing = new AIModelPricing @@ -2607,71 +3184,76 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "z-ai/glm-4.7-flash", + Model = "deepseek/deepseek-v3.2", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9285, - ContextLimit = 202752, - Created = new DateTime(2026, 1, 19), + Rank = 9220, + ContextLimit = 163840, + Created = new DateTime(2025, 12, 1), Pricing = new AIModelPricing { - Prompt = 0.00000006m, - Completion = 0.0000004m, - InputCacheRead = 0.00000001m, + Prompt = 0.00000026m, + Completion = 0.00000038m, + InputCacheRead = 0.00000013m, }, }, new AIModelCapabilities { Provider = provider, - Model = "deepseek/deepseek-v3.2", + Model = "z-ai/glm-4.7-flash", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9280, - ContextLimit = 163840, - Created = new DateTime(2025, 12, 1), + Rank = 9215, + ContextLimit = 202752, + Created = new DateTime(2026, 1, 19), Pricing = new AIModelPricing { - Prompt = 0.000000269m, + Prompt = 0.00000006m, Completion = 0.0000004m, - InputCacheRead = 0.0000001345m, + InputCacheRead = 0.00000001m, }, }, new AIModelCapabilities { Provider = provider, - Model = "allenai/olmo-3-32b-think", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, + Model = "upstage/solar-pro-3", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9275, - ContextLimit = 65536, - Created = new DateTime(2025, 11, 21), + Rank = 9210, + ContextLimit = 131072, + Created = new DateTime(2026, 1, 27), Pricing = new AIModelPricing { Prompt = 0.00000015m, - Completion = 0.0000005m, + Completion = 0.0000006m, + InputCacheRead = 0.000000015m, }, }, new AIModelCapabilities { Provider = provider, - Model = "upstage/solar-pro-3", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "qwen/qwen3-coder-next", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9270, - ContextLimit = 128000, - Created = new DateTime(2026, 1, 27), + Rank = 9205, + ContextLimit = 262144, + Created = new DateTime(2026, 2, 4), Pricing = new AIModelPricing { - Prompt = 0.00000015m, - Completion = 0.0000006m, - InputCacheRead = 0.000000015m, + Prompt = 0.00000012m, + Completion = 0.0000008m, + InputCacheRead = 0.00000007m, }, }, @@ -2681,8 +3263,9 @@ public override Task> RetrieveModels() Model = "z-ai/glm-4.6v", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9265, + Rank = 9200, ContextLimit = 131072, Created = new DateTime(2025, 12, 8), Pricing = new AIModelPricing @@ -2693,14 +3276,34 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "minimax/minimax-m2.5", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 9195, + ContextLimit = 204800, + Created = new DateTime(2026, 2, 12), + Pricing = new AIModelPricing + { + Prompt = 0.00000027m, + Completion = 0.00000108m, + InputCacheRead = 0.000000027m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "minimax/minimax-m2-her", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9260, + Rank = 9190, ContextLimit = 65536, Created = new DateTime(2026, 1, 23), Pricing = new AIModelPricing @@ -2717,8 +3320,9 @@ public override Task> RetrieveModels() Model = "minimax/minimax-m2.1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9255, + Rank = 9185, ContextLimit = 204800, Created = new DateTime(2025, 12, 23), Pricing = new AIModelPricing @@ -2732,35 +3336,38 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "deepcogito/cogito-v2.1-671b", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, + Model = "mistralai/mistral-large-2512", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9250, - ContextLimit = 128000, - Created = new DateTime(2025, 11, 13), + Rank = 9180, + ContextLimit = 262144, + Created = new DateTime(2025, 12, 1), Pricing = new AIModelPricing { - Prompt = 0.00000125m, - Completion = 0.00000125m, + Prompt = 0.0000005m, + Completion = 0.0000015m, + InputCacheRead = 0.00000005m, }, }, new AIModelCapabilities { Provider = provider, - Model = "mistralai/mistral-large-2512", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + Model = "qwen/qwen3.5-plus-02-15", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9245, - ContextLimit = 262144, - Created = new DateTime(2025, 12, 1), + Rank = 9175, + ContextLimit = 1000000, + Created = new DateTime(2026, 2, 16), Pricing = new AIModelPricing { - Prompt = 0.0000005m, - Completion = 0.0000015m, - InputCacheRead = 0.00000005m, + Prompt = 0.00000026m, + Completion = 0.00000156m, + InputCacheWrite = 0.000000325m, }, }, @@ -2770,8 +3377,9 @@ public override Task> RetrieveModels() Model = "z-ai/glm-4.7", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9240, + Rank = 9170, ContextLimit = 204800, Created = new DateTime(2025, 12, 22), Pricing = new AIModelPricing @@ -2785,89 +3393,93 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "bytedance-seed/seed-1.6", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "z-ai/glm-5", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9235, - ContextLimit = 262144, - Created = new DateTime(2025, 12, 23), + Rank = 9165, + ContextLimit = 204800, + Created = new DateTime(2026, 2, 11), Pricing = new AIModelPricing { - Prompt = 0.00000025m, - Completion = 0.000002m, + Prompt = 0.0000006m, + Completion = 0.00000192m, + InputCacheRead = 0.00000012m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.1-codex-mini", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "bytedance-seed/seed-1.6", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9230, - ContextLimit = 400000, - Created = new DateTime(2025, 11, 13), + Rank = 9160, + ContextLimit = 262144, + Created = new DateTime(2025, 12, 23), Pricing = new AIModelPricing { Prompt = 0.00000025m, Completion = 0.000002m, - InputCacheRead = 0.00000003m, - WebSearch = 0.01m, }, }, new AIModelCapabilities { Provider = provider, - Model = "amazon/nova-2-lite-v1", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + Model = "moonshotai/kimi-k2.5", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9225, - ContextLimit = 1000000, - Created = new DateTime(2025, 12, 2), + Rank = 9155, + ContextLimit = 262144, + Created = new DateTime(2026, 1, 27), Pricing = new AIModelPricing { - Prompt = 0.0000003m, - Completion = 0.0000025m, + Prompt = 0.00000045m, + Completion = 0.00000225m, + InputCacheRead = 0.00000007m, }, }, new AIModelCapabilities { Provider = provider, - Model = "moonshotai/kimi-k2-thinking", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "qwen/qwen3.5-397b-a17b", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9220, + Rank = 9150, ContextLimit = 262144, - Created = new DateTime(2025, 11, 6), + Created = new DateTime(2026, 2, 16), Pricing = new AIModelPricing { - Prompt = 0.0000006m, - Completion = 0.0000025m, - InputCacheRead = 0.00000015m, + Prompt = 0.00000039m, + Completion = 0.00000234m, + InputCacheRead = 0.000000111m, }, }, new AIModelCapabilities { Provider = provider, - Model = "moonshotai/kimi-k2.5", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "amazon/nova-2-lite-v1", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9215, - ContextLimit = 262144, - Created = new DateTime(2026, 1, 27), + Rank = 9145, + ContextLimit = 1000000, + Created = new DateTime(2025, 12, 2), Pricing = new AIModelPricing { - Prompt = 0.00000057m, - Completion = 0.00000285m, - InputCacheRead = 0.000000095m, + Prompt = 0.0000003m, + Completion = 0.0000025m, }, }, @@ -2877,8 +3489,9 @@ public override Task> RetrieveModels() Model = "relace/relace-search", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9210, + Rank = 9140, ContextLimit = 256000, Created = new DateTime(2025, 12, 8), Pricing = new AIModelPricing @@ -2888,14 +3501,33 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "qwen/qwen3-max-thinking", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 9135, + ContextLimit = 262144, + Created = new DateTime(2026, 2, 9), + Pricing = new AIModelPricing + { + Prompt = 0.00000078m, + Completion = 0.0000039m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "google/gemini-3-flash-preview", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9205, + Rank = 9130, ContextLimit = 1048576, Created = new DateTime(2025, 12, 17), Pricing = new AIModelPricing @@ -2906,10 +3538,20 @@ public override Task> RetrieveModels() Audio = 0.000001m, InputAudioCache = 0.0000001m, InputCacheRead = 0.00000005m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.000003m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000025m, + Completion = 0.0000015m, + Image = 0.00000025m, + Audio = 0.0000005m, + InternalReasoning = 0.0000015m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-3-flash-preview:batch" }, }, new AIModelCapabilities @@ -2918,8 +3560,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-audio-mini", Capabilities = AICapability.TextInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9200, + Rank = 9125, ContextLimit = 128000, Created = new DateTime(2026, 1, 19), Pricing = new AIModelPricing @@ -2937,70 +3580,34 @@ public override Task> RetrieveModels() Model = "writer/palmyra-x5", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9195, + Rank = 9120, ContextLimit = 1040000, Created = new DateTime(2026, 1, 21), Pricing = new AIModelPricing { - Prompt = 0.0000006m, - Completion = 0.000006m, - }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "openai/gpt-5.1-codex-max", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9190, - ContextLimit = 400000, - Created = new DateTime(2025, 12, 4), - Pricing = new AIModelPricing - { - Prompt = 0.00000125m, - Completion = 0.00001m, - InputCacheRead = 0.000000125m, - WebSearch = 0.01m, - }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "openai/gpt-5.1", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9185, - ContextLimit = 400000, - Created = new DateTime(2025, 11, 13), - Pricing = new AIModelPricing - { - Prompt = 0.00000125m, - Completion = 0.00001m, - InputCacheRead = 0.000000125m, - WebSearch = 0.01m, + Prompt = 0.0000006m, + Completion = 0.000006m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.1-codex", + Model = "openai/gpt-5.1-codex-max", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9180, + Rank = 9115, ContextLimit = 400000, - Created = new DateTime(2025, 11, 13), + Created = new DateTime(2025, 12, 4), Pricing = new AIModelPricing { Prompt = 0.00000125m, Completion = 0.00001m, - InputCacheRead = 0.00000013m, + InputCacheRead = 0.000000125m, WebSearch = 0.01m, }, }, @@ -3011,8 +3618,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5.2-codex", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9175, + Rank = 9110, ContextLimit = 400000, Created = new DateTime(2026, 1, 14), Pricing = new AIModelPricing @@ -3030,8 +3638,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5.2", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9170, + Rank = 9105, ContextLimit = 400000, Created = new DateTime(2025, 12, 10), Pricing = new AIModelPricing @@ -3041,63 +3650,62 @@ public override Task> RetrieveModels() InputCacheRead = 0.000000175m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000875m, + Completion = 0.000007m, + InputCacheRead = 0.0000000875m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.2:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "anthropic/claude-opus-4.5", + Model = "anthropic/claude-sonnet-4.6", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9165, - ContextLimit = 200000, - Created = new DateTime(2025, 11, 24), + Rank = 9100, + ContextLimit = 1000000, + Created = new DateTime(2026, 2, 17), Pricing = new AIModelPricing { - Prompt = 0.000005m, - Completion = 0.000025m, - InputCacheRead = 0.0000005m, - InputCacheWrite = 0.00000625m, + Prompt = 0.000003m, + Completion = 0.000015m, + InputCacheRead = 0.0000003m, + InputCacheWrite = 0.00000375m, WebSearch = 0.01m, }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "openai/gpt-audio", - Capabilities = AICapability.TextInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 9160, - ContextLimit = 128000, - Created = new DateTime(2026, 1, 19), - Pricing = new AIModelPricing + BatchPricing = new AIModelPricing { - Prompt = 0.0000025m, - Completion = 0.00001m, - Audio = 0.000032m, - AudioOutput = 0.000064m, + Prompt = 0.0000015m, + Completion = 0.0000075m, + InputCacheRead = 0.00000015m, + InputCacheWrite = 0.000001875m, + WebSearch = 0.01m, }, + Aliases = new List { "anthropic/claude-sonnet-4.6:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "google/gemini-3-pro-image-preview", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, + Model = "google/gemini-3.1-pro-preview", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9155, - ContextLimit = 65536, - Created = new DateTime(2025, 11, 20), + Rank = 9095, + ContextLimit = 1048576, + Created = new DateTime(2026, 2, 19), Pricing = new AIModelPricing { Prompt = 0.000002m, Completion = 0.000012m, Image = 0.000002m, - ImageOutput = 0.00012m, Audio = 0.000002m, InputAudioCache = 0.0000002m, InputCacheRead = 0.0000002m, @@ -3105,115 +3713,143 @@ public override Task> RetrieveModels() InternalReasoning = 0.000012m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000006m, + Image = 0.000001m, + Audio = 0.000001m, + InternalReasoning = 0.000006m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-3.1-pro-preview:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.2-pro", + Model = "anthropic/claude-opus-4.6", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9150, - ContextLimit = 400000, - Created = new DateTime(2025, 12, 10), + Rank = 9090, + ContextLimit = 1000000, + Created = new DateTime(2026, 2, 4), Pricing = new AIModelPricing { - Prompt = 0.000021m, - Completion = 0.000168m, + Prompt = 0.000005m, + Completion = 0.000025m, + InputCacheRead = 0.0000005m, + InputCacheWrite = 0.00000625m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.0000125m, + InputCacheRead = 0.00000025m, + InputCacheWrite = 0.000003125m, WebSearch = 0.01m, }, + Aliases = new List { "anthropic/claude-opus-4.6:batch" }, }, - - - // Released between August 2025 and November 2025 - new AIModelCapabilities { Provider = provider, - Model = "nvidia/nemotron-nano-12b-v2-vl:free", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + Model = "anthropic/claude-opus-4.5", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9145, - ContextLimit = 128000, - Created = new DateTime(2025, 10, 28), + Rank = 9085, + ContextLimit = 200000, + Created = new DateTime(2025, 11, 24), + Pricing = new AIModelPricing + { + Prompt = 0.000005m, + Completion = 0.000025m, + InputCacheRead = 0.0000005m, + InputCacheWrite = 0.00000625m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.0000125m, + InputCacheRead = 0.00000025m, + InputCacheWrite = 0.000003125m, + WebSearch = 0.01m, + }, + Aliases = new List { "anthropic/claude-opus-4.5:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "nvidia/nemotron-nano-9b-v2:free", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "openai/gpt-audio", + Capabilities = AICapability.TextInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9140, + Rank = 9080, ContextLimit = 128000, - Created = new DateTime(2025, 9, 5), - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "openai/gpt-oss-20b:free", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9135, - ContextLimit = 131072, - Created = new DateTime(2025, 8, 5), - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "ibm-granite/granite-4.0-h-micro", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 9130, - ContextLimit = 131000, - Created = new DateTime(2025, 10, 20), + Created = new DateTime(2026, 1, 19), Pricing = new AIModelPricing { - Prompt = 0.000000017m, - Completion = 0.000000112m, + Prompt = 0.0000025m, + Completion = 0.00001m, + Audio = 0.000032m, + AudioOutput = 0.000064m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-oss-20b", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "openai/gpt-5.2-pro", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 9125, - ContextLimit = 131072, - Created = new DateTime(2025, 8, 5), + Rank = 9075, + ContextLimit = 400000, + Created = new DateTime(2025, 12, 10), Pricing = new AIModelPricing { - Prompt = 0.00000003m, - Completion = 0.00000013m, - InputCacheRead = 0.00000003m, + Prompt = 0.000021m, + Completion = 0.000168m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000105m, + Completion = 0.000084m, + WebSearch = 0.01m, }, + Aliases = new List { "openai/gpt-5.2-pro:batch" }, }, + + + // Released between August 2025 and November 2025 + new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-oss-120b", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "ibm-granite/granite-4.0-h-micro", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9120, - ContextLimit = 131072, - Created = new DateTime(2025, 8, 5), + Rank = 9070, + ContextLimit = 131000, + Created = new DateTime(2025, 10, 20), Pricing = new AIModelPricing { - Prompt = 0.000000037m, - Completion = 0.00000017m, + Prompt = 0.000000017m, + Completion = 0.000000112m, }, }, @@ -3223,8 +3859,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-oss-safeguard-20b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9115, + Rank = 9065, ContextLimit = 131072, Created = new DateTime(2025, 10, 29), Pricing = new AIModelPricing @@ -3241,8 +3878,9 @@ public override Task> RetrieveModels() Model = "nousresearch/hermes-4-70b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9110, + Rank = 9060, ContextLimit = 131072, Created = new DateTime(2025, 8, 26), Pricing = new AIModelPricing @@ -3252,33 +3890,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "openai/gpt-5-nano", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9105, - ContextLimit = 400000, - Created = new DateTime(2025, 8, 7), - Pricing = new AIModelPricing - { - Prompt = 0.00000005m, - Completion = 0.0000004m, - InputCacheRead = 0.000000005m, - WebSearch = 0.01m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "deepseek/deepseek-v3.2-exp", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9100, + Rank = 9055, ContextLimit = 163840, Created = new DateTime(2025, 9, 29), Pricing = new AIModelPricing @@ -3294,8 +3914,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-vl-32b-instruct", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9095, + Rank = 9050, ContextLimit = 131072, Created = new DateTime(2025, 10, 23), Pricing = new AIModelPricing @@ -3311,8 +3932,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-vl-8b-instruct", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9090, + Rank = 9045, ContextLimit = 262144, Created = new DateTime(2025, 10, 14), Pricing = new AIModelPricing @@ -3322,14 +3944,33 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "allenai/olmo-3-32b-think", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 9040, + ContextLimit = 65536, + Created = new DateTime(2025, 11, 21), + Pricing = new AIModelPricing + { + Prompt = 0.00000015m, + Completion = 0.0000005m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "thedrummer/cydonia-24b-v4.1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9085, + Rank = 9035, ContextLimit = 131072, Created = new DateTime(2025, 9, 27), Pricing = new AIModelPricing @@ -3346,8 +3987,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-vl-30b-a3b-instruct", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9080, + Rank = 9030, ContextLimit = 262144, Created = new DateTime(2025, 10, 6), Pricing = new AIModelPricing @@ -3363,8 +4005,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen-plus-2025-07-28", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9075, + Rank = 9025, ContextLimit = 1000000, Created = new DateTime(2025, 9, 8), Pricing = new AIModelPricing @@ -3378,18 +4021,19 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "deepseek/deepseek-chat-v3.1", + Model = "qwen/qwen-plus-2025-07-28:thinking", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, - Verified = false, - Rank = 9070, - ContextLimit = 163840, - Created = new DateTime(2025, 8, 21), + SupportsBatch = false, + Verified = false, + Rank = 9020, + ContextLimit = 1000000, + Created = new DateTime(2025, 9, 8), Pricing = new AIModelPricing { - Prompt = 0.00000025m, - Completion = 0.00000095m, - InputCacheRead = 0.00000013m, + Prompt = 0.00000026m, + Completion = 0.00000078m, + InputCacheWrite = 0.0000005m, }, }, @@ -3399,8 +4043,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-coder-flash", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9065, + Rank = 9015, ContextLimit = 1000000, Created = new DateTime(2025, 9, 17), Pricing = new AIModelPricing @@ -3418,8 +4063,9 @@ public override Task> RetrieveModels() Model = "deepseek/deepseek-v3.1-terminus", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9060, + Rank = 9010, ContextLimit = 163840, Created = new DateTime(2025, 9, 22), Pricing = new AIModelPricing @@ -3436,8 +4082,9 @@ public override Task> RetrieveModels() Model = "minimax/minimax-m2", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9055, + Rank = 9005, ContextLimit = 204800, Created = new DateTime(2025, 10, 23), Pricing = new AIModelPricing @@ -3454,8 +4101,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-next-80b-a3b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9050, + Rank = 9000, ContextLimit = 262144, Created = new DateTime(2025, 9, 11), Pricing = new AIModelPricing @@ -3472,8 +4120,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-next-80b-a3b-thinking", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9045, + Rank = 8995, ContextLimit = 262144, Created = new DateTime(2025, 9, 11), Pricing = new AIModelPricing @@ -3483,32 +4132,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "qwen/qwen-plus-2025-07-28:thinking", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 9040, - ContextLimit = 1000000, - Created = new DateTime(2025, 9, 8), - Pricing = new AIModelPricing - { - Prompt = 0.0000004m, - Completion = 0.0000012m, - InputCacheWrite = 0.0000005m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "relace/relace-apply-3", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9035, + Rank = 8990, ContextLimit = 256000, Created = new DateTime(2025, 9, 26), Pricing = new AIModelPricing @@ -3524,8 +4156,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-vl-235b-a22b-instruct", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9030, + Rank = 8985, ContextLimit = 262144, Created = new DateTime(2025, 9, 23), Pricing = new AIModelPricing @@ -3539,37 +4172,19 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "mistralai/mistral-medium-3.1", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 9025, - ContextLimit = 131072, - Created = new DateTime(2025, 8, 13), - Pricing = new AIModelPricing - { - Prompt = 0.0000004m, - Completion = 0.000002m, - InputCacheRead = 0.00000004m, - }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "openai/gpt-5-mini", + Model = "openai/gpt-5.1-codex-mini", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - Default = AICapability.Text2Text | AICapability.Text2Json, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9020, + Rank = 8980, ContextLimit = 400000, - Created = new DateTime(2025, 8, 7), + Created = new DateTime(2025, 11, 13), Pricing = new AIModelPricing { Prompt = 0.00000025m, Completion = 0.000002m, - InputCacheRead = 0.000000025m, + InputCacheRead = 0.00000003m, WebSearch = 0.01m, }, }, @@ -3580,8 +4195,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-vl-8b-thinking", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9015, + Rank = 8975, ContextLimit = 131072, Created = new DateTime(2025, 10, 14), Pricing = new AIModelPricing @@ -3597,8 +4213,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-vl-30b-a3b-thinking", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9010, + Rank = 8970, ContextLimit = 262144, Created = new DateTime(2025, 10, 6), Pricing = new AIModelPricing @@ -3614,8 +4231,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-30b-a3b-thinking-2507", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9005, + Rank = 8965, ContextLimit = 81920, Created = new DateTime(2025, 8, 28), Pricing = new AIModelPricing @@ -3626,14 +4244,34 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "moonshotai/kimi-k2-thinking", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8960, + ContextLimit = 262144, + Created = new DateTime(2025, 11, 6), + Pricing = new AIModelPricing + { + Prompt = 0.0000006m, + Completion = 0.0000025m, + InputCacheRead = 0.00000015m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "nousresearch/hermes-4-405b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 9000, + Rank = 8955, ContextLimit = 131072, Created = new DateTime(2025, 8, 26), Pricing = new AIModelPricing @@ -3649,8 +4287,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-coder-plus", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8995, + Rank = 8950, ContextLimit = 1000000, Created = new DateTime(2025, 9, 23), Pricing = new AIModelPricing @@ -3668,8 +4307,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-max", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8990, + Rank = 8945, ContextLimit = 262144, Created = new DateTime(2025, 9, 23), Pricing = new AIModelPricing @@ -3687,8 +4327,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-vl-235b-a22b-thinking", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8985, + Rank = 8940, ContextLimit = 131072, Created = new DateTime(2025, 9, 23), Pricing = new AIModelPricing @@ -3704,8 +4345,9 @@ public override Task> RetrieveModels() Model = "anthropic/claude-haiku-4.5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8980, + Rank = 8935, ContextLimit = 200000, Created = new DateTime(2025, 10, 15), Pricing = new AIModelPricing @@ -3716,23 +4358,15 @@ public override Task> RetrieveModels() InputCacheWrite = 0.00000125m, WebSearch = 0.01m, }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "ai21/jamba-large-1.7", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 8975, - ContextLimit = 256000, - Created = new DateTime(2025, 8, 8), - Pricing = new AIModelPricing + BatchPricing = new AIModelPricing { - Prompt = 0.000002m, - Completion = 0.000008m, + Prompt = 0.0000005m, + Completion = 0.0000025m, + InputCacheRead = 0.00000005m, + InputCacheWrite = 0.000000625m, + WebSearch = 0.01m, }, + Aliases = new List { "anthropic/claude-haiku-4.5:batch" }, }, new AIModelCapabilities @@ -3741,8 +4375,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5-image-mini", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8970, + Rank = 8930, ContextLimit = 400000, Created = new DateTime(2025, 10, 16), Pricing = new AIModelPricing @@ -3758,13 +4393,14 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5", + Model = "openai/gpt-5.1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8965, + Rank = 8925, ContextLimit = 400000, - Created = new DateTime(2025, 8, 7), + Created = new DateTime(2025, 11, 13), Pricing = new AIModelPricing { Prompt = 0.00000125m, @@ -3772,6 +4408,34 @@ public override Task> RetrieveModels() InputCacheRead = 0.000000125m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000625m, + Completion = 0.000005m, + InputCacheRead = 0.0000000625m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5.1:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-5.1-codex", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8920, + ContextLimit = 400000, + Created = new DateTime(2025, 11, 13), + Pricing = new AIModelPricing + { + Prompt = 0.00000125m, + Completion = 0.00001m, + InputCacheRead = 0.00000013m, + WebSearch = 0.01m, + }, }, new AIModelCapabilities @@ -3780,8 +4444,9 @@ public override Task> RetrieveModels() Model = "amazon/nova-premier-v1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8960, + Rank = 8915, ContextLimit = 1000000, Created = new DateTime(2025, 10, 31), Pricing = new AIModelPricing @@ -3798,8 +4463,9 @@ public override Task> RetrieveModels() Model = "perplexity/sonar-pro-search", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8955, + Rank = 8910, ContextLimit = 200000, Created = new DateTime(2025, 10, 30), Pricing = new AIModelPricing @@ -3816,8 +4482,9 @@ public override Task> RetrieveModels() Model = "anthropic/claude-sonnet-4.5", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8950, + Rank = 8905, ContextLimit = 1000000, Created = new DateTime(2025, 9, 29), Pricing = new AIModelPricing @@ -3828,6 +4495,15 @@ public override Task> RetrieveModels() InputCacheWrite = 0.00000375m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000015m, + Completion = 0.0000075m, + InputCacheRead = 0.00000015m, + InputCacheWrite = 0.000001875m, + WebSearch = 0.01m, + }, + Aliases = new List { "anthropic/claude-sonnet-4.5:batch" }, }, new AIModelCapabilities @@ -3836,8 +4512,9 @@ public override Task> RetrieveModels() Model = "google/gemini-2.5-flash-image", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8945, + Rank = 8900, ContextLimit = 32768, Created = new DateTime(2025, 10, 7), Pricing = new AIModelPricing @@ -3849,7 +4526,7 @@ public override Task> RetrieveModels() Audio = 0.000001m, InputAudioCache = 0.0000001m, InputCacheRead = 0.00000003m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000025m, WebSearch = 0.014m, }, @@ -3861,8 +4538,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5-image", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8940, + Rank = 8895, ContextLimit = 400000, Created = new DateTime(2025, 10, 14), Pricing = new AIModelPricing @@ -3875,34 +4553,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "anthropic/claude-opus-4.1", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 8935, - ContextLimit = 200000, - Created = new DateTime(2025, 8, 5), - Pricing = new AIModelPricing - { - Prompt = 0.000015m, - Completion = 0.000075m, - InputCacheRead = 0.0000015m, - InputCacheWrite = 0.00001875m, - WebSearch = 0.01m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "mistralai/voxtral-small-24b-2507", Capabilities = AICapability.TextInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8930, + Rank = 8890, ContextLimit = 32000, Created = new DateTime(2025, 10, 30), Pricing = new AIModelPricing @@ -3920,8 +4579,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-5-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8925, + Rank = 8885, ContextLimit = 400000, Created = new DateTime(2025, 10, 6), Pricing = new AIModelPricing @@ -3930,6 +4590,39 @@ public override Task> RetrieveModels() Completion = 0.00012m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000075m, + Completion = 0.00006m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5-pro:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "google/gemini-3-pro-image-preview", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8880, + ContextLimit = 65536, + Created = new DateTime(2025, 11, 20), + Pricing = new AIModelPricing + { + Prompt = 0.000002m, + Completion = 0.000012m, + Image = 0.000002m, + ImageOutput = 0.00012m, + Audio = 0.000002m, + InputAudioCache = 0.0000002m, + InputCacheRead = 0.0000002m, + InputCacheWrite = 0.000000375m, + InternalReasoning = 0.000012m, + WebSearch = 0.014m, + }, }, @@ -3939,17 +4632,38 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "google/gemma-3n-e4b-it", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + Model = "openai/gpt-oss-20b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8920, - ContextLimit = 32768, - Created = new DateTime(2025, 5, 20), + Rank = 8875, + ContextLimit = 131072, + Created = new DateTime(2025, 8, 5), Pricing = new AIModelPricing { - Prompt = 0.00000006m, - Completion = 0.00000012m, + Prompt = 0.00000003m, + Completion = 0.00000013m, + InputCacheRead = 0.00000003m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-oss-120b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8870, + ContextLimit = 131072, + Created = new DateTime(2025, 8, 5), + Pricing = new AIModelPricing + { + Prompt = 0.000000037m, + Completion = 0.00000017m, + InputCacheRead = 0.00000003m, }, }, @@ -3959,8 +4673,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-30b-a3b-instruct-2507", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8915, + Rank = 8865, ContextLimit = 262144, Created = new DateTime(2025, 7, 29), Pricing = new AIModelPricing @@ -3976,8 +4691,9 @@ public override Task> RetrieveModels() Model = "bytedance/ui-tars-1.5-7b", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8910, + Rank = 8860, ContextLimit = 128000, Created = new DateTime(2025, 7, 22), Pricing = new AIModelPricing @@ -3994,9 +4710,10 @@ public override Task> RetrieveModels() Model = "mistralai/mistral-small-3.2-24b-instruct", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8905, - ContextLimit = 256000, + Rank = 8855, + ContextLimit = 131072, Created = new DateTime(2025, 6, 20), Pricing = new AIModelPricing { @@ -4012,8 +4729,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-coder-30b-a3b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8900, + Rank = 8850, ContextLimit = 262144, Created = new DateTime(2025, 7, 31), Pricing = new AIModelPricing @@ -4023,14 +4741,43 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-5-nano", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = true, + Verified = false, + Rank = 8845, + ContextLimit = 400000, + Created = new DateTime(2025, 8, 7), + Pricing = new AIModelPricing + { + Prompt = 0.00000005m, + Completion = 0.0000004m, + InputCacheRead = 0.000000005m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000025m, + Completion = 0.0000002m, + InputCacheRead = 0.0000000025m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5-nano:batch" }, + }, + new AIModelCapabilities { Provider = provider, Model = "qwen/qwen3-235b-a22b-2507", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8895, + Rank = 8840, ContextLimit = 262144, Created = new DateTime(2025, 7, 21), Pricing = new AIModelPricing @@ -4046,8 +4793,9 @@ public override Task> RetrieveModels() Model = "tencent/hunyuan-a13b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8890, + Rank = 8835, ContextLimit = 131072, Created = new DateTime(2025, 7, 8), Pricing = new AIModelPricing @@ -4063,8 +4811,9 @@ public override Task> RetrieveModels() Model = "google/gemini-2.5-flash-lite", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8885, + Rank = 8830, ContextLimit = 1048576, Created = new DateTime(2025, 7, 22), Pricing = new AIModelPricing @@ -4075,10 +4824,22 @@ public override Task> RetrieveModels() Audio = 0.0000003m, InputAudioCache = 0.00000003m, InputCacheRead = 0.00000001m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000004m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000005m, + Completion = 0.0000002m, + Image = 0.00000005m, + Audio = 0.00000015m, + InputAudioCache = 0.00000003m, + InputCacheRead = 0.00000001m, + InternalReasoning = 0.0000002m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-2.5-flash-lite:batch" }, }, new AIModelCapabilities @@ -4087,8 +4848,9 @@ public override Task> RetrieveModels() Model = "z-ai/glm-4.5-air", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8880, + Rank = 8825, ContextLimit = 131072, Created = new DateTime(2025, 7, 25), Pricing = new AIModelPricing @@ -4105,8 +4867,9 @@ public override Task> RetrieveModels() Model = "mistralai/codestral-2508", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8875, + Rank = 8820, ContextLimit = 256000, Created = new DateTime(2025, 8, 1), Pricing = new AIModelPricing @@ -4122,8 +4885,9 @@ public override Task> RetrieveModels() Provider = provider, Model = "cognitivecomputations/dolphin-mistral-24b-venice-edition", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + SupportsBatch = false, Verified = false, - Rank = 8870, + Rank = 8815, ContextLimit = 128000, Created = new DateTime(2025, 7, 9), Pricing = new AIModelPricing @@ -4139,8 +4903,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-coder", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8865, + Rank = 8810, ContextLimit = 262144, Created = new DateTime(2025, 7, 23), Pricing = new AIModelPricing @@ -4157,8 +4922,9 @@ public override Task> RetrieveModels() Model = "morph/morph-v3-fast", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8860, + Rank = 8805, ContextLimit = 81920, Created = new DateTime(2025, 7, 7), Pricing = new AIModelPricing @@ -4171,34 +4937,37 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "arcee-ai/virtuoso-large", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, + Model = "baidu/ernie-4.5-vl-424b-a47b", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8855, - ContextLimit = 131072, - Created = new DateTime(2025, 5, 5), + Rank = 8800, + ContextLimit = 123000, + Created = new DateTime(2025, 6, 30), Pricing = new AIModelPricing { - Prompt = 0.00000075m, - Completion = 0.0000012m, + Prompt = 0.00000042m, + Completion = 0.00000125m, }, }, new AIModelCapabilities { Provider = provider, - Model = "baidu/ernie-4.5-vl-424b-a47b", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.Reasoning, + Model = "deepseek/deepseek-chat-v3.1", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8850, - ContextLimit = 123000, - Created = new DateTime(2025, 6, 30), + Rank = 8795, + ContextLimit = 163840, + Created = new DateTime(2025, 8, 21), Pricing = new AIModelPricing { - Prompt = 0.00000042m, - Completion = 0.00000125m, + Prompt = 0.00000055m, + Completion = 0.00000165m, + InputCacheRead = 0.00000055m, }, }, @@ -4208,8 +4977,9 @@ public override Task> RetrieveModels() Model = "morph/morph-v3-large", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8845, + Rank = 8790, ContextLimit = 262144, Created = new DateTime(2025, 7, 7), Pricing = new AIModelPricing @@ -4222,13 +4992,14 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "mistralai/mistral-medium-3", + Model = "mistralai/mistral-medium-3.1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8840, + Rank = 8785, ContextLimit = 131072, - Created = new DateTime(2025, 5, 7), + Created = new DateTime(2025, 8, 13), Pricing = new AIModelPricing { Prompt = 0.0000004m, @@ -4237,14 +5008,44 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-5-mini", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Default = AICapability.Text2Text | AICapability.Text2Json, + SupportsStreaming = true, + SupportsBatch = true, + Verified = false, + Rank = 8780, + ContextLimit = 400000, + Created = new DateTime(2025, 8, 7), + Pricing = new AIModelPricing + { + Prompt = 0.00000025m, + Completion = 0.000002m, + InputCacheRead = 0.000000025m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000125m, + Completion = 0.000001m, + InputCacheRead = 0.0000000125m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5-mini:batch" }, + }, + new AIModelCapabilities { Provider = provider, Model = "deepseek/deepseek-r1-0528", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8835, + Rank = 8775, ContextLimit = 163840, Created = new DateTime(2025, 5, 28), Pricing = new AIModelPricing @@ -4261,8 +5062,9 @@ public override Task> RetrieveModels() Model = "minimax/minimax-m1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8830, + Rank = 8770, ContextLimit = 1000000, Created = new DateTime(2025, 6, 17), Pricing = new AIModelPricing @@ -4278,8 +5080,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-235b-a22b-thinking-2507", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8825, + Rank = 8765, ContextLimit = 262144, Created = new DateTime(2025, 7, 25), Pricing = new AIModelPricing @@ -4296,8 +5099,9 @@ public override Task> RetrieveModels() Model = "moonshotai/kimi-k2", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8820, + Rank = 8760, ContextLimit = 131072, Created = new DateTime(2025, 7, 11), Pricing = new AIModelPricing @@ -4313,8 +5117,9 @@ public override Task> RetrieveModels() Model = "google/gemini-2.5-flash", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8815, + Rank = 8755, ContextLimit = 1048576, Created = new DateTime(2025, 6, 17), Pricing = new AIModelPricing @@ -4325,46 +5130,63 @@ public override Task> RetrieveModels() Audio = 0.000001m, InputAudioCache = 0.0000001m, InputCacheRead = 0.00000003m, - InputCacheWrite = 0.00000008333333333333334m, + InputCacheWrite = 0.0000000833333333333333m, InternalReasoning = 0.0000025m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000015m, + Completion = 0.00000125m, + Image = 0.00000015m, + Audio = 0.0000005m, + InputAudioCache = 0.0000001m, + InputCacheRead = 0.00000003m, + InternalReasoning = 0.00000125m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-2.5-flash:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "google/gemini-2.5-pro", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "openai/gpt-5", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8810, - ContextLimit = 1048576, - Created = new DateTime(2025, 6, 17), + Rank = 8750, + ContextLimit = 400000, + Created = new DateTime(2025, 8, 7), Pricing = new AIModelPricing { Prompt = 0.00000125m, Completion = 0.00001m, - Image = 0.00000125m, - Audio = 0.00000125m, - InputAudioCache = 0.000000125m, InputCacheRead = 0.000000125m, - InputCacheWrite = 0.000000375m, - InternalReasoning = 0.00001m, - WebSearch = 0.014m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000625m, + Completion = 0.000005m, + InputCacheRead = 0.0000000625m, + WebSearch = 0.01m, }, + Aliases = new List { "openai/gpt-5:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "google/gemini-2.5-pro-preview", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "google/gemini-2.5-pro", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8805, + Rank = 8745, ContextLimit = 1048576, - Created = new DateTime(2025, 6, 5), + Created = new DateTime(2025, 6, 17), Pricing = new AIModelPricing { Prompt = 0.00000125m, @@ -4377,18 +5199,31 @@ public override Task> RetrieveModels() InternalReasoning = 0.00001m, WebSearch = 0.014m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000625m, + Completion = 0.000005m, + Image = 0.000000625m, + Audio = 0.000000625m, + InputAudioCache = 0.000000125m, + InputCacheRead = 0.000000125m, + InternalReasoning = 0.000005m, + WebSearch = 0.014m, + }, + Aliases = new List { "google/gemini-2.5-pro:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "google/gemini-2.5-pro-preview-05-06", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "google/gemini-2.5-pro-preview", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8800, + Rank = 8740, ContextLimit = 1048576, - Created = new DateTime(2025, 5, 7), + Created = new DateTime(2025, 6, 5), Pricing = new AIModelPricing { Prompt = 0.00000125m, @@ -4406,33 +5241,14 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "anthropic/claude-sonnet-4", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 8795, - ContextLimit = 1000000, - Created = new DateTime(2025, 5, 22), - Pricing = new AIModelPricing - { - Prompt = 0.000003m, - Completion = 0.000015m, - InputCacheRead = 0.0000003m, - InputCacheWrite = 0.00000375m, - WebSearch = 0.01m, - }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "anthropic/claude-opus-4", + Model = "anthropic/claude-opus-4.1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8790, + Rank = 8735, ContextLimit = 200000, - Created = new DateTime(2025, 5, 22), + Created = new DateTime(2025, 8, 5), Pricing = new AIModelPricing { Prompt = 0.000015m, @@ -4441,6 +5257,15 @@ public override Task> RetrieveModels() InputCacheWrite = 0.00001875m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000075m, + Completion = 0.0000375m, + InputCacheRead = 0.00000075m, + InputCacheWrite = 0.000009375m, + WebSearch = 0.01m, + }, + Aliases = new List { "anthropic/claude-opus-4.1:batch" }, }, new AIModelCapabilities @@ -4449,8 +5274,9 @@ public override Task> RetrieveModels() Model = "openai/o3-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8785, + Rank = 8730, ContextLimit = 200000, Created = new DateTime(2025, 6, 10), Pricing = new AIModelPricing @@ -4459,6 +5285,13 @@ public override Task> RetrieveModels() Completion = 0.00008m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00001m, + Completion = 0.00004m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/o3-pro:batch" }, }, @@ -4471,8 +5304,9 @@ public override Task> RetrieveModels() Model = "google/gemma-3-4b-it", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8780, + Rank = 8725, ContextLimit = 131072, Created = new DateTime(2025, 3, 13), Pricing = new AIModelPricing @@ -4482,14 +5316,33 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "google/gemma-3n-e4b-it", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8720, + ContextLimit = 32768, + Created = new DateTime(2025, 5, 20), + Pricing = new AIModelPricing + { + Prompt = 0.00000006m, + Completion = 0.00000012m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "google/gemma-3-12b-it", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8775, + Rank = 8715, ContextLimit = 131072, Created = new DateTime(2025, 3, 13), Pricing = new AIModelPricing @@ -4505,8 +5358,9 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-guard-4-12b", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8770, + Rank = 8710, ContextLimit = 1048576, Created = new DateTime(2025, 4, 30), Pricing = new AIModelPricing @@ -4522,14 +5376,33 @@ public override Task> RetrieveModels() Model = "rekaai/reka-flash-3", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8765, + Rank = 8705, ContextLimit = 65536, Created = new DateTime(2025, 3, 12), Pricing = new AIModelPricing { - Prompt = 0.0000001m, - Completion = 0.0000002m, + Prompt = 0.0000001m, + Completion = 0.0000002m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "qwen/qwen3-14b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8700, + ContextLimit = 131072, + Created = new DateTime(2025, 4, 28), + Pricing = new AIModelPricing + { + Prompt = 0.00000012m, + Completion = 0.00000024m, }, }, @@ -4539,8 +5412,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-32b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8760, + Rank = 8695, ContextLimit = 131072, Created = new DateTime(2025, 4, 28), Pricing = new AIModelPricing @@ -4557,8 +5431,9 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-4-scout", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8755, + Rank = 8690, ContextLimit = 1310720, Created = new DateTime(2025, 4, 5), Pricing = new AIModelPricing @@ -4574,8 +5449,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4.1-nano", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8750, + Rank = 8685, ContextLimit = 1047576, Created = new DateTime(2025, 4, 14), Pricing = new AIModelPricing @@ -4585,6 +5461,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.000000025m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000005m, + Completion = 0.0000002m, + InputCacheRead = 0.0000000125m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-4.1-nano:batch" }, }, new AIModelCapabilities @@ -4593,8 +5477,9 @@ public override Task> RetrieveModels() Model = "google/gemma-3-27b-it", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8745, + Rank = 8680, ContextLimit = 262144, Created = new DateTime(2025, 3, 12), Pricing = new AIModelPricing @@ -4611,8 +5496,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-8b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8740, + Rank = 8675, ContextLimit = 131072, Created = new DateTime(2025, 4, 28), Pricing = new AIModelPricing @@ -4629,8 +5515,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-30b-a3b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8735, + Rank = 8670, ContextLimit = 131072, Created = new DateTime(2025, 4, 28), Pricing = new AIModelPricing @@ -4646,8 +5533,9 @@ public override Task> RetrieveModels() Model = "mistralai/mistral-small-3.1-24b-instruct", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8730, + Rank = 8665, ContextLimit = 128000, Created = new DateTime(2025, 3, 17), Pricing = new AIModelPricing @@ -4657,32 +5545,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "mistralai/mistral-saba", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 8725, - ContextLimit = 32768, - Created = new DateTime(2025, 2, 17), - Pricing = new AIModelPricing - { - Prompt = 0.0000002m, - Completion = 0.0000006m, - InputCacheRead = 0.00000002m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "meta-llama/llama-4-maverick", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8720, + Rank = 8660, ContextLimit = 1048576, Created = new DateTime(2025, 4, 5), Pricing = new AIModelPricing @@ -4698,8 +5569,9 @@ public override Task> RetrieveModels() Model = "thedrummer/skyfall-36b-v2", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8715, + Rank = 8655, ContextLimit = 32768, Created = new DateTime(2025, 3, 10), Pricing = new AIModelPricing @@ -4713,35 +5585,37 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3-14b", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "deepseek/deepseek-chat-v3-0324", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8710, - ContextLimit = 131072, - Created = new DateTime(2025, 4, 28), + Rank = 8650, + ContextLimit = 163840, + Created = new DateTime(2025, 3, 24), Pricing = new AIModelPricing { - Prompt = 0.0000002275m, - Completion = 0.00000091m, + Prompt = 0.00000025m, + Completion = 0.000001m, + InputCacheRead = 0.000000135m, }, }, new AIModelCapabilities { Provider = provider, - Model = "deepseek/deepseek-chat-v3-0324", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + Model = "arcee-ai/virtuoso-large", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8705, - ContextLimit = 163840, - Created = new DateTime(2025, 3, 24), + Rank = 8645, + ContextLimit = 131072, + Created = new DateTime(2025, 5, 5), Pricing = new AIModelPricing { - Prompt = 0.00000027m, - Completion = 0.00000112m, - InputCacheRead = 0.000000135m, + Prompt = 0.00000075m, + Completion = 0.0000012m, }, }, @@ -4751,8 +5625,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4.1-mini", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8700, + Rank = 8640, ContextLimit = 1047576, Created = new DateTime(2025, 4, 14), Pricing = new AIModelPricing @@ -4762,39 +5637,50 @@ public override Task> RetrieveModels() InputCacheRead = 0.0000001m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000002m, + Completion = 0.0000008m, + InputCacheRead = 0.00000005m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-4.1-mini:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "aion-labs/aion-rp-llama-3.1-8b", - Capabilities = AICapability.TextInput | AICapability.TextOutput, + Model = "qwen/qwen3-235b-a22b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8695, - ContextLimit = 32768, - Created = new DateTime(2025, 2, 4), + Rank = 8635, + ContextLimit = 131072, + Created = new DateTime(2025, 4, 28), Pricing = new AIModelPricing { - Prompt = 0.0000008m, - Completion = 0.0000016m, + Prompt = 0.000000455m, + Completion = 0.00000182m, }, }, new AIModelCapabilities { Provider = provider, - Model = "qwen/qwen3-235b-a22b", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "mistralai/mistral-medium-3", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8690, + Rank = 8630, ContextLimit = 131072, - Created = new DateTime(2025, 4, 28), + Created = new DateTime(2025, 5, 7), Pricing = new AIModelPricing { - Prompt = 0.000000455m, - Completion = 0.00000182m, + Prompt = 0.0000004m, + Completion = 0.000002m, + InputCacheRead = 0.00000004m, }, }, @@ -4804,8 +5690,9 @@ public override Task> RetrieveModels() Model = "openai/o4-mini-high", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8685, + Rank = 8625, ContextLimit = 200000, Created = new DateTime(2025, 4, 16), Pricing = new AIModelPricing @@ -4815,6 +5702,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.000000275m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000055m, + Completion = 0.0000022m, + InputCacheRead = 0.0000001375m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/o4-mini-high:batch" }, }, new AIModelCapabilities @@ -4823,8 +5718,9 @@ public override Task> RetrieveModels() Model = "openai/o4-mini", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8680, + Rank = 8620, ContextLimit = 200000, Created = new DateTime(2025, 4, 16), Pricing = new AIModelPricing @@ -4834,25 +5730,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.000000275m, WebSearch = 0.01m, }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "openai/o3-mini-high", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, - SupportsStreaming = true, - Verified = false, - Rank = 8675, - ContextLimit = 200000, - Created = new DateTime(2025, 2, 12), - Pricing = new AIModelPricing + BatchPricing = new AIModelPricing { - Prompt = 0.0000011m, - Completion = 0.0000044m, - InputCacheRead = 0.00000055m, + Prompt = 0.00000055m, + Completion = 0.0000022m, + InputCacheRead = 0.0000001375m, WebSearch = 0.01m, }, + Aliases = new List { "openai/o4-mini:batch" }, }, new AIModelCapabilities @@ -4861,8 +5746,9 @@ public override Task> RetrieveModels() Model = "openai/o3", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8670, + Rank = 8615, ContextLimit = 200000, Created = new DateTime(2025, 4, 16), Pricing = new AIModelPricing @@ -4872,6 +5758,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.0000005m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000004m, + InputCacheRead = 0.00000025m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/o3:batch" }, }, new AIModelCapabilities @@ -4880,8 +5774,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4.1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8665, + Rank = 8610, ContextLimit = 1047576, Created = new DateTime(2025, 4, 14), Pricing = new AIModelPricing @@ -4891,6 +5786,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.0000005m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000004m, + InputCacheRead = 0.00000025m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-4.1:batch" }, }, new AIModelCapabilities @@ -4899,8 +5802,9 @@ public override Task> RetrieveModels() Model = "perplexity/sonar-reasoning-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8660, + Rank = 8605, ContextLimit = 128000, Created = new DateTime(2025, 3, 7), Pricing = new AIModelPricing @@ -4917,8 +5821,9 @@ public override Task> RetrieveModels() Model = "perplexity/sonar-deep-research", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8655, + Rank = 8600, ContextLimit = 128000, Created = new DateTime(2025, 3, 7), Pricing = new AIModelPricing @@ -4936,8 +5841,9 @@ public override Task> RetrieveModels() Model = "cohere/command-a", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8650, + Rank = 8595, ContextLimit = 256000, Created = new DateTime(2025, 3, 13), Pricing = new AIModelPricing @@ -4947,14 +5853,61 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "google/gemini-2.5-pro-preview-05-06", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8590, + ContextLimit = 1048576, + Created = new DateTime(2025, 5, 7), + Pricing = new AIModelPricing + { + Prompt = 0.00000125m, + Completion = 0.00001m, + Image = 0.00000125m, + Audio = 0.00000125m, + InputAudioCache = 0.000000125m, + InputCacheRead = 0.000000125m, + InputCacheWrite = 0.000000375m, + InternalReasoning = 0.00001m, + WebSearch = 0.014m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "anthropic/claude-sonnet-4", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8585, + ContextLimit = 1000000, + Created = new DateTime(2025, 5, 22), + Pricing = new AIModelPricing + { + Prompt = 0.000003m, + Completion = 0.000015m, + InputCacheRead = 0.0000003m, + InputCacheWrite = 0.00000375m, + WebSearch = 0.01m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "perplexity/sonar-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8645, + Rank = 8580, ContextLimit = 200000, Created = new DateTime(2025, 3, 7), Pricing = new AIModelPricing @@ -4965,14 +5918,36 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "anthropic/claude-opus-4", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8575, + ContextLimit = 200000, + Created = new DateTime(2025, 5, 22), + Pricing = new AIModelPricing + { + Prompt = 0.000015m, + Completion = 0.000075m, + InputCacheRead = 0.0000015m, + InputCacheWrite = 0.00001875m, + WebSearch = 0.01m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "openai/o1-pro", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8640, + Rank = 8570, ContextLimit = 200000, Created = new DateTime(2025, 3, 19), Pricing = new AIModelPricing @@ -4981,6 +5956,13 @@ public override Task> RetrieveModels() Completion = 0.0006m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000075m, + Completion = 0.0003m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/o1-pro:batch" }, }, @@ -4993,8 +5975,9 @@ public override Task> RetrieveModels() Model = "mistralai/mistral-small-24b-instruct-2501", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8635, + Rank = 8565, ContextLimit = 32768, Created = new DateTime(2025, 1, 30), Pricing = new AIModelPricing @@ -5010,8 +5993,9 @@ public override Task> RetrieveModels() Model = "microsoft/phi-4", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8630, + Rank = 8560, ContextLimit = 16384, Created = new DateTime(2025, 1, 10), Pricing = new AIModelPricing @@ -5027,8 +6011,9 @@ public override Task> RetrieveModels() Model = "amazon/nova-micro-v1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8625, + Rank = 8555, ContextLimit = 128000, Created = new DateTime(2024, 12, 5), Pricing = new AIModelPricing @@ -5044,8 +6029,9 @@ public override Task> RetrieveModels() Model = "cohere/command-r7b-12-2024", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8620, + Rank = 8550, ContextLimit = 128000, Created = new DateTime(2024, 12, 14), Pricing = new AIModelPricing @@ -5061,8 +6047,9 @@ public override Task> RetrieveModels() Model = "amazon/nova-lite-v1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8615, + Rank = 8545, ContextLimit = 300000, Created = new DateTime(2024, 12, 5), Pricing = new AIModelPricing @@ -5078,30 +6065,34 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3.3-70b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8610, + Rank = 8540, ContextLimit = 131072, Created = new DateTime(2024, 12, 6), Pricing = new AIModelPricing { - Prompt = 0.00000013m, - Completion = 0.0000004m, + Prompt = 0.0000001m, + Completion = 0.00000032m, }, }, new AIModelCapabilities { Provider = provider, - Model = "thedrummer/unslopnemo-12b", + Model = "mistralai/mistral-saba", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8605, - ContextLimit = 1024000, - Created = new DateTime(2024, 11, 8), + Rank = 8535, + ContextLimit = 32768, + Created = new DateTime(2025, 2, 17), Pricing = new AIModelPricing { - Prompt = 0.0000004m, - Completion = 0.0000004m, + Prompt = 0.0000002m, + Completion = 0.0000006m, + InputCacheRead = 0.00000002m, }, }, @@ -5111,8 +6102,9 @@ public override Task> RetrieveModels() Model = "sao10k/l3.3-euryale-70b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8600, + Rank = 8530, ContextLimit = 131072, Created = new DateTime(2024, 12, 18), Pricing = new AIModelPricing @@ -5128,8 +6120,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen-plus", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8595, + Rank = 8525, ContextLimit = 1000000, Created = new DateTime(2025, 2, 1), Pricing = new AIModelPricing @@ -5147,8 +6140,9 @@ public override Task> RetrieveModels() Model = "deepseek/deepseek-r1-distill-llama-70b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8590, + Rank = 8520, ContextLimit = 8192, Created = new DateTime(2025, 1, 23), Pricing = new AIModelPricing @@ -5164,8 +6158,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen2.5-vl-72b-instruct", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8585, + Rank = 8515, ContextLimit = 128000, Created = new DateTime(2025, 2, 1), Pricing = new AIModelPricing @@ -5182,8 +6177,9 @@ public override Task> RetrieveModels() Model = "perplexity/sonar", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8580, + Rank = 8510, ContextLimit = 127072, Created = new DateTime(2025, 1, 27), Pricing = new AIModelPricing @@ -5194,31 +6190,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "qwen/qwen-2.5-coder-32b-instruct", - Capabilities = AICapability.TextInput | AICapability.TextOutput, - SupportsStreaming = true, - Verified = false, - Rank = 8575, - ContextLimit = 32768, - Created = new DateTime(2024, 11, 11), - Pricing = new AIModelPricing - { - Prompt = 0.00000066m, - Completion = 0.000001m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "deepseek/deepseek-chat", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8570, + Rank = 8505, ContextLimit = 163840, Created = new DateTime(2024, 12, 26), Pricing = new AIModelPricing @@ -5234,8 +6214,9 @@ public override Task> RetrieveModels() Model = "minimax/minimax-01", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8565, + Rank = 8500, ContextLimit = 1000192, Created = new DateTime(2025, 1, 15), Pricing = new AIModelPricing @@ -5245,15 +6226,34 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "aion-labs/aion-rp-llama-3.1-8b", + Capabilities = AICapability.TextInput | AICapability.TextOutput, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8495, + ContextLimit = 32768, + Created = new DateTime(2025, 2, 4), + Pricing = new AIModelPricing + { + Prompt = 0.0000008m, + Completion = 0.0000016m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "deepseek/deepseek-r1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8560, - ContextLimit = 163840, + Rank = 8490, + ContextLimit = 64000, Created = new DateTime(2025, 1, 20), Pricing = new AIModelPricing { @@ -5268,8 +6268,9 @@ public override Task> RetrieveModels() Model = "amazon/nova-pro-v1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8555, + Rank = 8485, ContextLimit = 300000, Created = new DateTime(2024, 12, 5), Pricing = new AIModelPricing @@ -5282,13 +6283,14 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "openai/o3-mini", + Model = "openai/o3-mini-high", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8550, + Rank = 8480, ContextLimit = 200000, - Created = new DateTime(2025, 1, 31), + Created = new DateTime(2025, 2, 12), Pricing = new AIModelPricing { Prompt = 0.0000011m, @@ -5296,42 +6298,42 @@ public override Task> RetrieveModels() InputCacheRead = 0.00000055m, WebSearch = 0.01m, }, - }, - - new AIModelCapabilities - { - Provider = provider, - Model = "mistralai/mistral-large-2407", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 8545, - ContextLimit = 131072, - Created = new DateTime(2024, 11, 19), - Pricing = new AIModelPricing + BatchPricing = new AIModelPricing { - Prompt = 0.000002m, - Completion = 0.000006m, - InputCacheRead = 0.0000002m, + Prompt = 0.00000055m, + Completion = 0.0000022m, + InputCacheRead = 0.000000275m, + WebSearch = 0.01m, }, + Aliases = new List { "openai/o3-mini-high:batch" }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-4o-2024-11-20", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + Model = "openai/o3-mini", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8540, - ContextLimit = 128000, - Created = new DateTime(2024, 11, 20), + Rank = 8475, + ContextLimit = 200000, + Created = new DateTime(2025, 1, 31), Pricing = new AIModelPricing { - Prompt = 0.0000025m, - Completion = 0.00001m, - InputCacheRead = 0.00000125m, + Prompt = 0.0000011m, + Completion = 0.0000044m, + InputCacheRead = 0.00000055m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000055m, + Completion = 0.0000022m, + InputCacheRead = 0.000000275m, + WebSearch = 0.01m, }, + Aliases = new List { "openai/o3-mini:batch" }, }, new AIModelCapabilities @@ -5340,8 +6342,9 @@ public override Task> RetrieveModels() Model = "openai/o1", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8535, + Rank = 8470, ContextLimit = 200000, Created = new DateTime(2024, 12, 17), Pricing = new AIModelPricing @@ -5351,6 +6354,14 @@ public override Task> RetrieveModels() InputCacheRead = 0.0000075m, WebSearch = 0.01m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.0000075m, + Completion = 0.00003m, + InputCacheRead = 0.00000375m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/o1:batch" }, }, @@ -5360,17 +6371,17 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "sao10k/l3-lunaris-8b", + Model = "mistralai/ministral-8b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, - SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8530, - ContextLimit = 8192, - Created = new DateTime(2024, 8, 13), + Rank = 8465, + ContextLimit = 128000, + Created = new DateTime(2024, 10, 17), Pricing = new AIModelPricing { - Prompt = 0.00000004m, - Completion = 0.00000005m, + Prompt = 0.00000011m, + Completion = 0.00000011m, }, }, @@ -5380,8 +6391,9 @@ public override Task> RetrieveModels() Model = "qwen/qwen-2.5-7b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8525, + Rank = 8460, ContextLimit = 32768, Created = new DateTime(2024, 10, 16), Pricing = new AIModelPricing @@ -5397,8 +6409,9 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3.2-1b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8520, + Rank = 8455, ContextLimit = 60000, Created = new DateTime(2024, 9, 25), Pricing = new AIModelPricing @@ -5414,8 +6427,9 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3.2-3b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8515, + Rank = 8450, ContextLimit = 131072, Created = new DateTime(2024, 9, 25), Pricing = new AIModelPricing @@ -5425,14 +6439,32 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "thedrummer/unslopnemo-12b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsBatch = false, + Verified = false, + Rank = 8445, + ContextLimit = 1024000, + Created = new DateTime(2024, 11, 8), + Pricing = new AIModelPricing + { + Prompt = 0.0000004m, + Completion = 0.0000004m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "qwen/qwen-2.5-72b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8510, + Rank = 8440, ContextLimit = 32768, Created = new DateTime(2024, 9, 19), Pricing = new AIModelPricing @@ -5448,8 +6480,9 @@ public override Task> RetrieveModels() Model = "thedrummer/rocinante-12b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8505, + Rank = 8435, ContextLimit = 65536, Created = new DateTime(2024, 9, 30), Pricing = new AIModelPricing @@ -5465,8 +6498,9 @@ public override Task> RetrieveModels() Model = "cohere/command-r-08-2024", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8500, + Rank = 8430, ContextLimit = 128000, Created = new DateTime(2024, 8, 30), Pricing = new AIModelPricing @@ -5476,31 +6510,15 @@ public override Task> RetrieveModels() }, }, - new AIModelCapabilities - { - Provider = provider, - Model = "nousresearch/hermes-3-llama-3.1-70b", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, - SupportsStreaming = true, - Verified = false, - Rank = 8495, - ContextLimit = 131072, - Created = new DateTime(2024, 8, 18), - Pricing = new AIModelPricing - { - Prompt = 0.0000007m, - Completion = 0.0000007m, - }, - }, - new AIModelCapabilities { Provider = provider, Model = "sao10k/l3.1-euryale-70b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8490, + Rank = 8425, ContextLimit = 131072, Created = new DateTime(2024, 8, 28), Pricing = new AIModelPricing @@ -5513,16 +6531,17 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "nousresearch/hermes-3-llama-3.1-405b", - Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + Model = "qwen/qwen-2.5-coder-32b-instruct", + Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8485, - ContextLimit = 131072, - Created = new DateTime(2024, 8, 16), + Rank = 8420, + ContextLimit = 32768, + Created = new DateTime(2024, 11, 11), Pricing = new AIModelPricing { - Prompt = 0.000001m, + Prompt = 0.00000066m, Completion = 0.000001m, }, }, @@ -5533,9 +6552,10 @@ public override Task> RetrieveModels() Model = "anthracite-org/magnum-v4-72b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8480, - ContextLimit = 16384, + Rank = 8415, + ContextLimit = 32768, Created = new DateTime(2024, 10, 22), Pricing = new AIModelPricing { @@ -5547,35 +6567,56 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "cohere/command-r-plus-08-2024", + Model = "mistralai/mistral-large-2407", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8475, + Rank = 8410, + ContextLimit = 131072, + Created = new DateTime(2024, 11, 19), + Pricing = new AIModelPricing + { + Prompt = 0.000002m, + Completion = 0.000006m, + InputCacheRead = 0.0000002m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-4o-2024-11-20", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8405, ContextLimit = 128000, - Created = new DateTime(2024, 8, 30), + Created = new DateTime(2024, 11, 20), Pricing = new AIModelPricing { Prompt = 0.0000025m, Completion = 0.00001m, + InputCacheRead = 0.00000125m, }, }, new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-4o-2024-08-06", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + Model = "cohere/command-r-plus-08-2024", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8470, + Rank = 8400, ContextLimit = 128000, - Created = new DateTime(2024, 8, 6), + Created = new DateTime(2024, 8, 30), Pricing = new AIModelPricing { Prompt = 0.0000025m, Completion = 0.00001m, - InputCacheRead = 0.00000125m, }, }, @@ -5589,8 +6630,9 @@ public override Task> RetrieveModels() Model = "openrouter/auto", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.ImageOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8465, + Rank = 8395, ContextLimit = 2000000, Created = new DateTime(2023, 11, 8), Pricing = new AIModelPricing @@ -5606,8 +6648,9 @@ public override Task> RetrieveModels() Model = "mistralai/mistral-nemo", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8460, + Rank = 8390, ContextLimit = 131072, Created = new DateTime(2024, 7, 19), Pricing = new AIModelPricing @@ -5617,14 +6660,33 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "sao10k/l3-lunaris-8b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8385, + ContextLimit = 8192, + Created = new DateTime(2024, 8, 13), + Pricing = new AIModelPricing + { + Prompt = 0.00000004m, + Completion = 0.00000005m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "gryphe/mythomax-l2-13b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8455, + Rank = 8380, ContextLimit = 8192, Created = new DateTime(2023, 7, 2), Pricing = new AIModelPricing @@ -5640,8 +6702,9 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3.1-8b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8450, + Rank = 8375, ContextLimit = 131072, Created = new DateTime(2024, 7, 23), Pricing = new AIModelPricing @@ -5658,8 +6721,9 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3.1-70b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8445, + Rank = 8370, ContextLimit = 131072, Created = new DateTime(2024, 7, 23), Pricing = new AIModelPricing @@ -5675,8 +6739,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4o-mini", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8440, + Rank = 8365, ContextLimit = 128000, Created = new DateTime(2024, 7, 18), Pricing = new AIModelPricing @@ -5685,6 +6750,14 @@ public override Task> RetrieveModels() Completion = 0.0000006m, InputCacheRead = 0.000000075m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000075m, + Completion = 0.0000003m, + InputCacheRead = 0.0000000375m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-4o-mini:batch" }, }, new AIModelCapabilities @@ -5693,8 +6766,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4o-mini-2024-07-18", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8435, + Rank = 8360, ContextLimit = 128000, Created = new DateTime(2024, 7, 18), Pricing = new AIModelPricing @@ -5711,8 +6785,9 @@ public override Task> RetrieveModels() Model = "microsoft/wizardlm-2-8x22b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8430, + Rank = 8355, ContextLimit = 65535, Created = new DateTime(2024, 4, 16), Pricing = new AIModelPricing @@ -5728,8 +6803,9 @@ public override Task> RetrieveModels() Model = "google/gemma-2-27b-it", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8425, + Rank = 8350, ContextLimit = 8192, Created = new DateTime(2024, 7, 13), Pricing = new AIModelPricing @@ -5745,8 +6821,9 @@ public override Task> RetrieveModels() Model = "undi95/remm-slerp-l2-13b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8420, + Rank = 8345, ContextLimit = 6144, Created = new DateTime(2023, 7, 22), Pricing = new AIModelPricing @@ -5756,14 +6833,33 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "nousresearch/hermes-3-llama-3.1-70b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8340, + ContextLimit = 131072, + Created = new DateTime(2024, 8, 18), + Pricing = new AIModelPricing + { + Prompt = 0.0000007m, + Completion = 0.0000007m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "mancer/weaver", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8415, + Rank = 8335, ContextLimit = 8000, Created = new DateTime(2023, 8, 2), Pricing = new AIModelPricing @@ -5773,14 +6869,33 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "nousresearch/hermes-3-llama-3.1-405b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8330, + ContextLimit = 131072, + Created = new DateTime(2024, 8, 16), + Pricing = new AIModelPricing + { + Prompt = 0.000001m, + Completion = 0.000001m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "anthropic/claude-3-haiku", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8410, + Rank = 8325, ContextLimit = 200000, Created = new DateTime(2024, 3, 13), Pricing = new AIModelPricing @@ -5799,8 +6914,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-3.5-turbo", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8405, + Rank = 8320, ContextLimit = 16385, Created = new DateTime(2023, 5, 28), Pricing = new AIModelPricing @@ -5808,6 +6924,13 @@ public override Task> RetrieveModels() Prompt = 0.0000005m, Completion = 0.0000015m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000025m, + Completion = 0.00000075m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-3.5-turbo:batch" }, }, new AIModelCapabilities @@ -5816,8 +6939,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-3.5-turbo-0613", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8400, + Rank = 8315, ContextLimit = 4095, Created = new DateTime(2024, 1, 25), Pricing = new AIModelPricing @@ -5833,8 +6957,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-3.5-turbo-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8395, + Rank = 8310, ContextLimit = 4095, Created = new DateTime(2023, 9, 28), Pricing = new AIModelPricing @@ -5850,8 +6975,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-3.5-turbo-16k", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8390, + Rank = 8305, ContextLimit = 16385, Created = new DateTime(2023, 8, 28), Pricing = new AIModelPricing @@ -5867,8 +6993,9 @@ public override Task> RetrieveModels() Model = "mistralai/mixtral-8x22b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8385, + Rank = 8300, ContextLimit = 65536, Created = new DateTime(2024, 4, 17), Pricing = new AIModelPricing @@ -5885,8 +7012,9 @@ public override Task> RetrieveModels() Model = "mistralai/mistral-large", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8380, + Rank = 8295, ContextLimit = 128000, Created = new DateTime(2024, 2, 26), Pricing = new AIModelPricing @@ -5897,14 +7025,34 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-4o-2024-08-06", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Rank = 8290, + ContextLimit = 128000, + Created = new DateTime(2024, 8, 6), + Pricing = new AIModelPricing + { + Prompt = 0.0000025m, + Completion = 0.00001m, + InputCacheRead = 0.00000125m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "openai/gpt-4o", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8375, + Rank = 8285, ContextLimit = 128000, Created = new DateTime(2024, 5, 13), Pricing = new AIModelPricing @@ -5913,6 +7061,14 @@ public override Task> RetrieveModels() Completion = 0.00001m, InputCacheRead = 0.00000125m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.00000125m, + Completion = 0.000005m, + InputCacheRead = 0.000000625m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-4o:batch" }, }, new AIModelCapabilities @@ -5921,8 +7077,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4o-2024-05-13", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8370, + Rank = 8280, ContextLimit = 128000, Created = new DateTime(2024, 5, 13), Pricing = new AIModelPricing @@ -5938,8 +7095,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4-turbo", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = true, Verified = false, - Rank = 8365, + Rank = 8275, ContextLimit = 128000, Created = new DateTime(2024, 4, 9), Pricing = new AIModelPricing @@ -5947,6 +7105,13 @@ public override Task> RetrieveModels() Prompt = 0.00001m, Completion = 0.00003m, }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000005m, + Completion = 0.000015m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-4-turbo:batch" }, }, new AIModelCapabilities @@ -5955,8 +7120,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4-turbo-preview", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8360, + Rank = 8270, ContextLimit = 128000, Created = new DateTime(2024, 1, 25), Pricing = new AIModelPricing @@ -5972,8 +7138,9 @@ public override Task> RetrieveModels() Model = "openai/gpt-4", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, - Rank = 8355, + Rank = 8265, ContextLimit = 8191, Created = new DateTime(2023, 5, 28), Pricing = new AIModelPricing @@ -5987,15 +7154,87 @@ public override Task> RetrieveModels() // Deprecated models + new AIModelCapabilities + { + Provider = provider, + Model = "dots-studio/dots-3-note-preview:free", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = 0, + ContextLimit = 512000, + Created = new DateTime(2026, 8, 14), + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "inclusionai/ling-2.6-flash", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -5, + ContextLimit = 262144, + Created = new DateTime(2026, 4, 21), + Pricing = new AIModelPricing + { + Prompt = 0.00000001m, + Completion = 0.00000003m, + InputCacheRead = 0.000000002m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "inclusionai/ring-2.6-1t", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -10, + ContextLimit = 262144, + Created = new DateTime(2026, 5, 8), + Pricing = new AIModelPricing + { + Prompt = 0.000000075m, + Completion = 0.000000625m, + InputCacheRead = 0.000000015m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "inclusionai/ling-2.6-1t", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -15, + ContextLimit = 262144, + Created = new DateTime(2026, 4, 23), + Pricing = new AIModelPricing + { + Prompt = 0.000000075m, + Completion = 0.000000625m, + InputCacheRead = 0.000000015m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "z-ai/glm-5v-turbo", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = 0, + Rank = -20, ContextLimit = 202752, Created = new DateTime(2026, 4, 1), Pricing = new AIModelPricing @@ -6009,21 +7248,15 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.3-chat", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + Model = "nvidia/nemotron-3-nano-30b-a3b:free", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -5, - ContextLimit = 128000, - Created = new DateTime(2026, 3, 3), - Pricing = new AIModelPricing - { - Prompt = 0.00000175m, - Completion = 0.000014m, - InputCacheRead = 0.000000175m, - WebSearch = 0.01m, - }, + Rank = -25, + ContextLimit = 256000, + Created = new DateTime(2025, 12, 14), }, new AIModelCapabilities @@ -6032,9 +7265,10 @@ public override Task> RetrieveModels() Model = "openai/gpt-5.2-chat", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -10, + Rank = -30, ContextLimit = 128000, Created = new DateTime(2025, 12, 10), Pricing = new AIModelPricing @@ -6049,20 +7283,29 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "z-ai/glm-4.5v", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "nvidia/nemotron-nano-12b-v2-vl:free", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -15, - ContextLimit = 65536, - Created = new DateTime(2025, 8, 11), - Pricing = new AIModelPricing - { - Prompt = 0.0000006m, - Completion = 0.0000018m, - InputCacheRead = 0.00000011m, - }, + Rank = -35, + ContextLimit = 128000, + Created = new DateTime(2025, 10, 28), + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "nvidia/nemotron-nano-9b-v2:free", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -40, + ContextLimit = 128000, + Created = new DateTime(2025, 9, 5), }, new AIModelCapabilities @@ -6071,9 +7314,10 @@ public override Task> RetrieveModels() Model = "z-ai/glm-4.6", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -20, + Rank = -45, ContextLimit = 204800, Created = new DateTime(2025, 9, 30), Pricing = new AIModelPricing @@ -6090,9 +7334,10 @@ public override Task> RetrieveModels() Model = "moonshotai/kimi-k2-0905", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -25, + Rank = -50, ContextLimit = 262144, Created = new DateTime(2025, 9, 4), Pricing = new AIModelPricing @@ -6102,22 +7347,90 @@ public override Task> RetrieveModels() }, }, + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-5-codex", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = true, + Verified = false, + Deprecated = true, + Rank = -55, + ContextLimit = 400000, + Created = new DateTime(2025, 9, 23), + Pricing = new AIModelPricing + { + Prompt = 0.000000625m, + Completion = 0.000005m, + InputCacheRead = 0.0000000625m, + WebSearch = 0.01m, + }, + BatchPricing = new AIModelPricing + { + Prompt = 0.000000625m, + Completion = 0.000005m, + InputCacheRead = 0.0000000625m, + WebSearch = 0.01m, + }, + Aliases = new List { "openai/gpt-5-codex:batch" }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "z-ai/glm-4.5v", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -60, + ContextLimit = 65536, + Created = new DateTime(2025, 8, 11), + Pricing = new AIModelPricing + { + Prompt = 0.0000006m, + Completion = 0.0000018m, + InputCacheRead = 0.00000011m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "z-ai/glm-4.5", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -65, + ContextLimit = 131072, + Created = new DateTime(2025, 7, 25), + Pricing = new AIModelPricing + { + Prompt = 0.0000006m, + Completion = 0.0000022m, + InputCacheRead = 0.00000011m, + }, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "ai21/jamba-large-1.7", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, + SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -30, - ContextLimit = 131072, - Created = new DateTime(2025, 7, 25), + Rank = -70, + ContextLimit = 256000, Pricing = new AIModelPricing { - Prompt = 0.0000006m, - Completion = 0.0000022m, - InputCacheRead = 0.00000011m, + Prompt = 0.000002m, + Completion = 0.000008m, }, }, @@ -6126,9 +7439,10 @@ public override Task> RetrieveModels() Provider = provider, Model = "aion-labs/aion-1.0", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -35, + Rank = -75, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6143,9 +7457,10 @@ public override Task> RetrieveModels() Model = "aion-labs/aion-1.0-mini", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -40, + Rank = -80, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6160,9 +7475,10 @@ public override Task> RetrieveModels() Model = "alfredpros/codellama-7b-instruct-solidity", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -45, + Rank = -85, ContextLimit = 131072, }, @@ -6172,9 +7488,10 @@ public override Task> RetrieveModels() Model = "alibaba/tongyi-deepresearch-30b-a3b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -50, + Rank = -90, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6190,9 +7507,10 @@ public override Task> RetrieveModels() Model = "allenai/olmo-3.1-32b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -55, + Rank = -95, ContextLimit = 65536, }, @@ -6202,9 +7520,10 @@ public override Task> RetrieveModels() Model = "alpindale/goliath-120b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -60, + Rank = -100, ContextLimit = 6144, Pricing = new AIModelPricing { @@ -6219,9 +7538,10 @@ public override Task> RetrieveModels() Model = "anthropic/claude-3.5-haiku", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -65, + Rank = -105, ContextLimit = 200000, Pricing = new AIModelPricing { @@ -6239,9 +7559,10 @@ public override Task> RetrieveModels() Model = "anthropic/claude-3.7-sonnet", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -70, + Rank = -110, ContextLimit = 200000, Pricing = new AIModelPricing { @@ -6259,9 +7580,10 @@ public override Task> RetrieveModels() Model = "anthropic/claude-3.7-sonnet:thinking", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -75, + Rank = -115, ContextLimit = 200000, Pricing = new AIModelPricing { @@ -6279,9 +7601,10 @@ public override Task> RetrieveModels() Model = "anthropic/claude-opus-4.6-fast", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -80, + Rank = -120, ContextLimit = 1000000, Pricing = new AIModelPricing { @@ -6299,9 +7622,10 @@ public override Task> RetrieveModels() Model = "arcee-ai/coder-large", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -85, + Rank = -125, ContextLimit = 32768, Pricing = new AIModelPricing { @@ -6317,9 +7641,10 @@ public override Task> RetrieveModels() Model = "arcee-ai/maestro-reasoning", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -90, + Rank = -130, ContextLimit = 131072, }, @@ -6329,9 +7654,10 @@ public override Task> RetrieveModels() Model = "arcee-ai/spotlight", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -95, + Rank = -135, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6346,9 +7672,10 @@ public override Task> RetrieveModels() Model = "arcee-ai/trinity-large-preview", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -100, + Rank = -140, ContextLimit = 131000, Pricing = new AIModelPricing { @@ -6363,9 +7690,10 @@ public override Task> RetrieveModels() Model = "arcee-ai/trinity-mini", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -105, + Rank = -145, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6380,9 +7708,10 @@ public override Task> RetrieveModels() Model = "baidu/ernie-4.5-21b-a3b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -110, + Rank = -150, ContextLimit = 120000, Pricing = new AIModelPricing { @@ -6397,9 +7726,10 @@ public override Task> RetrieveModels() Model = "baidu/ernie-4.5-21b-a3b-thinking", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -115, + Rank = -155, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6414,9 +7744,10 @@ public override Task> RetrieveModels() Model = "baidu/ernie-4.5-300b-a47b", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -120, + Rank = -160, ContextLimit = 123000, Pricing = new AIModelPricing { @@ -6431,9 +7762,10 @@ public override Task> RetrieveModels() Model = "baidu/ernie-4.5-vl-28b-a3b", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -125, + Rank = -165, ContextLimit = 30000, Pricing = new AIModelPricing { @@ -6448,9 +7780,10 @@ public override Task> RetrieveModels() Model = "baidu/qianfan-ocr-fast:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -130, + Rank = -170, ContextLimit = 65536, }, @@ -6460,21 +7793,41 @@ public override Task> RetrieveModels() Model = "cognitivecomputations/dolphin-mistral-24b-venice-edition:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -135, + Rank = -175, ContextLimit = 32768, }, + new AIModelCapabilities + { + Provider = provider, + Model = "deepcogito/cogito-v2.1-671b", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -180, + ContextLimit = 128000, + Pricing = new AIModelPricing + { + Prompt = 0.00000125m, + Completion = 0.00000125m, + }, + }, + new AIModelCapabilities { Provider = provider, Model = "deepseek/deepseek-r1-distill-qwen-32b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -140, + Rank = -185, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -6489,9 +7842,10 @@ public override Task> RetrieveModels() Model = "deepseek/deepseek-v3.2-speciale", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -145, + Rank = -190, ContextLimit = 163840, }, @@ -6501,9 +7855,10 @@ public override Task> RetrieveModels() Model = "essentialai/rnj-1-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -150, + Rank = -195, ContextLimit = 32768, Pricing = new AIModelPricing { @@ -6518,9 +7873,10 @@ public override Task> RetrieveModels() Model = "google/gemini-2.0-flash-001", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -155, + Rank = -200, ContextLimit = 1000000, }, @@ -6530,9 +7886,10 @@ public override Task> RetrieveModels() Model = "google/gemini-2.0-flash-lite-001", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -160, + Rank = -205, ContextLimit = 1048576, Pricing = new AIModelPricing { @@ -6551,9 +7908,10 @@ public override Task> RetrieveModels() Model = "google/gemini-2.5-flash-lite-preview-09-2025", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -165, + Rank = -210, ContextLimit = 1048576, Pricing = new AIModelPricing { @@ -6574,9 +7932,10 @@ public override Task> RetrieveModels() Model = "google/gemma-3-12b-it:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -170, + Rank = -215, ContextLimit = 32768, }, @@ -6586,9 +7945,10 @@ public override Task> RetrieveModels() Model = "google/gemma-3-27b-it:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -175, + Rank = -220, ContextLimit = 131072, }, @@ -6598,9 +7958,10 @@ public override Task> RetrieveModels() Model = "google/gemma-3-4b-it:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -180, + Rank = -225, ContextLimit = 32768, }, @@ -6610,9 +7971,10 @@ public override Task> RetrieveModels() Model = "google/gemma-3n-e2b-it:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -185, + Rank = -230, ContextLimit = 8192, Pricing = new AIModelPricing { @@ -6627,9 +7989,10 @@ public override Task> RetrieveModels() Model = "google/gemma-3n-e4b-it:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -190, + Rank = -235, ContextLimit = 8192, }, @@ -6639,9 +8002,22 @@ public override Task> RetrieveModels() Model = "inclusionai/ling-2.6-1t:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -195, + Rank = -240, + ContextLimit = 262144, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "inclusionai/ling-3.0-flash:free", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -245, ContextLimit = 262144, }, @@ -6650,9 +8026,10 @@ public override Task> RetrieveModels() Provider = provider, Model = "inclusionai/ring-2.6-1t:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -200, + Rank = -250, ContextLimit = 262144, }, @@ -6662,9 +8039,10 @@ public override Task> RetrieveModels() Model = "inflection/inflection-3-pi", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -205, + Rank = -255, ContextLimit = 8000, Pricing = new AIModelPricing { @@ -6679,9 +8057,10 @@ public override Task> RetrieveModels() Model = "inflection/inflection-3-productivity", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -210, + Rank = -260, ContextLimit = 8000, Pricing = new AIModelPricing { @@ -6696,9 +8075,10 @@ public override Task> RetrieveModels() Model = "liquid/lfm-2-24b-a2b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -215, + Rank = -265, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -6713,9 +8093,10 @@ public override Task> RetrieveModels() Model = "liquid/lfm-2.5-1.2b-instruct:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -220, + Rank = -270, ContextLimit = 32768, }, @@ -6725,9 +8106,10 @@ public override Task> RetrieveModels() Model = "liquid/lfm-2.5-1.2b-thinking:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -225, + Rank = -275, ContextLimit = 32768, }, @@ -6737,9 +8119,10 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3-70b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -230, + Rank = -280, ContextLimit = 8192, Pricing = new AIModelPricing { @@ -6754,9 +8137,10 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3-8b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -235, + Rank = -285, ContextLimit = 8192, Pricing = new AIModelPricing { @@ -6771,9 +8155,10 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3.2-11b-vision-instruct", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -240, + Rank = -290, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6788,9 +8173,10 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3.2-3b-instruct:free", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -245, + Rank = -295, ContextLimit = 131072, }, @@ -6800,9 +8186,10 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-3.3-70b-instruct:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -250, + Rank = -300, ContextLimit = 131072, }, @@ -6812,9 +8199,10 @@ public override Task> RetrieveModels() Model = "meta-llama/llama-guard-3-8b", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -255, + Rank = -305, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6828,9 +8216,10 @@ public override Task> RetrieveModels() Provider = provider, Model = "microsoft/phi-4-mini-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -260, + Rank = -310, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6846,9 +8235,10 @@ public override Task> RetrieveModels() Model = "minimax/minimax-m2.5:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -265, + Rank = -315, ContextLimit = 196608, }, @@ -6858,9 +8248,10 @@ public override Task> RetrieveModels() Model = "mistralai/devstral-2512", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -270, + Rank = -320, ContextLimit = 262144, Pricing = new AIModelPricing { @@ -6876,9 +8267,10 @@ public override Task> RetrieveModels() Model = "mistralai/devstral-medium", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -275, + Rank = -325, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6894,9 +8286,10 @@ public override Task> RetrieveModels() Model = "mistralai/devstral-small", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -280, + Rank = -330, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6912,9 +8305,10 @@ public override Task> RetrieveModels() Model = "mistralai/mistral-7b-instruct-v0.1", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -285, + Rank = -335, ContextLimit = 2824, }, @@ -6924,9 +8318,10 @@ public override Task> RetrieveModels() Model = "mistralai/mistral-large-2411", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -290, + Rank = -340, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6942,9 +8337,10 @@ public override Task> RetrieveModels() Model = "mistralai/mixtral-8x7b-instruct", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -295, + Rank = -345, ContextLimit = 32768, }, @@ -6954,9 +8350,10 @@ public override Task> RetrieveModels() Model = "mistralai/pixtral-large-2411", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -300, + Rank = -350, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6972,9 +8369,10 @@ public override Task> RetrieveModels() Model = "nex-agi/deepseek-v3.1-nex-n1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -305, + Rank = -355, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -6988,9 +8386,10 @@ public override Task> RetrieveModels() Provider = provider, Model = "nex-agi/nex-n2-pro:free", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -310, + Rank = -360, ContextLimit = 262144, }, @@ -7000,9 +8399,10 @@ public override Task> RetrieveModels() Model = "nousresearch/hermes-2-pro-llama-3-8b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -315, + Rank = -365, ContextLimit = 8192, Pricing = new AIModelPricing { @@ -7017,9 +8417,10 @@ public override Task> RetrieveModels() Model = "nousresearch/hermes-3-llama-3.1-405b:free", Capabilities = AICapability.TextInput | AICapability.TextOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -320, + Rank = -370, ContextLimit = 131072, }, @@ -7029,9 +8430,10 @@ public override Task> RetrieveModels() Model = "nvidia/llama-3.3-nemotron-super-49b-v1.5", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -325, + Rank = -375, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -7046,9 +8448,10 @@ public override Task> RetrieveModels() Model = "nvidia/nemotron-nano-9b-v2", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -330, + Rank = -380, ContextLimit = 131072, }, @@ -7058,9 +8461,10 @@ public override Task> RetrieveModels() Model = "openai/gpt-4-0314", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -335, + Rank = -385, ContextLimit = 8191, Pricing = new AIModelPricing { @@ -7075,9 +8479,10 @@ public override Task> RetrieveModels() Model = "openai/gpt-4-1106-preview", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -340, + Rank = -390, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -7092,9 +8497,10 @@ public override Task> RetrieveModels() Model = "openai/gpt-4o-audio-preview", Capabilities = AICapability.TextInput | AICapability.AudioInput | AICapability.TextOutput | AICapability.AudioOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -345, + Rank = -395, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -7110,9 +8516,10 @@ public override Task> RetrieveModels() Model = "openai/gpt-4o-mini-search-preview", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -350, + Rank = -400, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -7128,9 +8535,10 @@ public override Task> RetrieveModels() Model = "openai/gpt-4o-search-preview", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -355, + Rank = -405, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -7146,9 +8554,10 @@ public override Task> RetrieveModels() Model = "openai/gpt-5-chat", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -360, + Rank = -410, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -7162,13 +8571,14 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5-codex", - Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + Model = "openai/gpt-5.1-chat", + Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -365, - ContextLimit = 400000, + Rank = -415, + ContextLimit = 128000, Pricing = new AIModelPricing { Prompt = 0.00000125m, @@ -7181,18 +8591,19 @@ public override Task> RetrieveModels() new AIModelCapabilities { Provider = provider, - Model = "openai/gpt-5.1-chat", + Model = "openai/gpt-5.3-chat", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -370, + Rank = -420, ContextLimit = 128000, Pricing = new AIModelPricing { - Prompt = 0.00000125m, - Completion = 0.00001m, - InputCacheRead = 0.000000125m, + Prompt = 0.00000175m, + Completion = 0.000014m, + InputCacheRead = 0.000000175m, WebSearch = 0.01m, }, }, @@ -7203,9 +8614,23 @@ public override Task> RetrieveModels() Model = "openai/gpt-oss-120b:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -375, + Rank = -425, + ContextLimit = 131072, + }, + + new AIModelCapabilities + { + Provider = provider, + Model = "openai/gpt-oss-20b:free", + Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsStreaming = true, + SupportsBatch = false, + Verified = false, + Deprecated = true, + Rank = -430, ContextLimit = 131072, }, @@ -7215,9 +8640,10 @@ public override Task> RetrieveModels() Model = "openai/o3-deep-research", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -380, + Rank = -435, ContextLimit = 200000, Pricing = new AIModelPricing { @@ -7234,9 +8660,10 @@ public override Task> RetrieveModels() Model = "openai/o4-mini-deep-research", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -385, + Rank = -440, ContextLimit = 200000, Pricing = new AIModelPricing { @@ -7253,9 +8680,10 @@ public override Task> RetrieveModels() Model = "openrouter/owl-alpha", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -390, + Rank = -445, ContextLimit = 1048756, }, @@ -7264,9 +8692,10 @@ public override Task> RetrieveModels() Provider = provider, Model = "poolside/laguna-m.1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -395, + Rank = -450, ContextLimit = 262144, Pricing = new AIModelPricing { @@ -7282,9 +8711,10 @@ public override Task> RetrieveModels() Model = "poolside/laguna-m.1:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -400, + Rank = -455, ContextLimit = 262144, }, @@ -7293,9 +8723,10 @@ public override Task> RetrieveModels() Provider = provider, Model = "poolside/laguna-xs.2", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -405, + Rank = -460, ContextLimit = 262144, Pricing = new AIModelPricing { @@ -7311,9 +8742,10 @@ public override Task> RetrieveModels() Model = "poolside/laguna-xs.2:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -410, + Rank = -465, ContextLimit = 262144, }, @@ -7323,9 +8755,10 @@ public override Task> RetrieveModels() Model = "prime-intellect/intellect-3", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -415, + Rank = -470, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -7340,9 +8773,10 @@ public override Task> RetrieveModels() Model = "qwen/qwen-max", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -420, + Rank = -475, ContextLimit = 32768, Pricing = new AIModelPricing { @@ -7358,9 +8792,10 @@ public override Task> RetrieveModels() Model = "qwen/qwen-turbo", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -425, + Rank = -480, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -7376,9 +8811,10 @@ public override Task> RetrieveModels() Model = "qwen/qwen-vl-max", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -430, + Rank = -485, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -7393,9 +8829,10 @@ public override Task> RetrieveModels() Model = "qwen/qwen-vl-plus", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -435, + Rank = -490, ContextLimit = 32768, }, @@ -7405,9 +8842,10 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-coder:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -440, + Rank = -495, ContextLimit = 1048576, }, @@ -7417,9 +8855,10 @@ public override Task> RetrieveModels() Model = "qwen/qwen3-next-80b-a3b-instruct:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -445, + Rank = -500, ContextLimit = 262144, }, @@ -7429,9 +8868,10 @@ public override Task> RetrieveModels() Model = "sao10k/l3-euryale-70b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -450, + Rank = -505, ContextLimit = 8192, Pricing = new AIModelPricing { @@ -7446,9 +8886,10 @@ public override Task> RetrieveModels() Model = "sao10k/l3.1-70b-hanami-x1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -455, + Rank = -510, ContextLimit = 16000, Pricing = new AIModelPricing { @@ -7463,9 +8904,10 @@ public override Task> RetrieveModels() Model = "switchpoint/router", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -460, + Rank = -515, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -7479,9 +8921,10 @@ public override Task> RetrieveModels() Provider = provider, Model = "tencent/hy3:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -465, + Rank = -520, ContextLimit = 262144, }, @@ -7491,9 +8934,10 @@ public override Task> RetrieveModels() Model = "tngtech/deepseek-r1t2-chimera", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -470, + Rank = -525, ContextLimit = 163840, }, @@ -7503,9 +8947,10 @@ public override Task> RetrieveModels() Model = "x-ai/grok-3", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -475, + Rank = -530, ContextLimit = 131072, }, @@ -7515,9 +8960,10 @@ public override Task> RetrieveModels() Model = "x-ai/grok-3-beta", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -480, + Rank = -535, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -7532,9 +8978,10 @@ public override Task> RetrieveModels() Model = "x-ai/grok-3-mini", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -485, + Rank = -540, ContextLimit = 131072, Pricing = new AIModelPricing { @@ -7549,9 +8996,10 @@ public override Task> RetrieveModels() Model = "x-ai/grok-3-mini-beta", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -490, + Rank = -545, ContextLimit = 1000000, }, @@ -7561,9 +9009,10 @@ public override Task> RetrieveModels() Model = "x-ai/grok-4", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -495, + Rank = -550, ContextLimit = 256000, }, @@ -7573,9 +9022,10 @@ public override Task> RetrieveModels() Model = "x-ai/grok-4-fast", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -500, + Rank = -555, ContextLimit = 262144, }, @@ -7585,9 +9035,10 @@ public override Task> RetrieveModels() Model = "x-ai/grok-4.1-fast", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -505, + Rank = -560, ContextLimit = 262144, }, @@ -7597,9 +9048,10 @@ public override Task> RetrieveModels() Model = "x-ai/grok-code-fast-1", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -510, + Rank = -565, ContextLimit = 65536, }, @@ -7609,9 +9061,10 @@ public override Task> RetrieveModels() Model = "xiaomi/mimo-v2-flash", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -515, + Rank = -570, ContextLimit = 262144, Pricing = new AIModelPricing { @@ -7627,9 +9080,10 @@ public override Task> RetrieveModels() Model = "xiaomi/mimo-v2-omni", Capabilities = AICapability.TextInput | AICapability.ImageInput | AICapability.AudioInput | AICapability.VideoInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -520, + Rank = -575, ContextLimit = 262144, Pricing = new AIModelPricing { @@ -7645,9 +9099,10 @@ public override Task> RetrieveModels() Model = "xiaomi/mimo-v2-pro", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.JsonOutput | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -525, + Rank = -580, ContextLimit = 1048576, Pricing = new AIModelPricing { @@ -7663,9 +9118,10 @@ public override Task> RetrieveModels() Model = "z-ai/glm-4-32b", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -530, + Rank = -585, ContextLimit = 128000, Pricing = new AIModelPricing { @@ -7680,9 +9136,10 @@ public override Task> RetrieveModels() Model = "z-ai/glm-4.5-air:free", Capabilities = AICapability.TextInput | AICapability.TextOutput | AICapability.FunctionCalling | AICapability.Reasoning, SupportsStreaming = true, + SupportsBatch = false, Verified = false, Deprecated = true, - Rank = -535, + Rank = -590, ContextLimit = 131072, } }; diff --git a/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderSettings.cs b/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderSettings.cs index f252ff58b..48135de83 100644 --- a/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderSettings.cs +++ b/src/SmartHopper.Providers.OpenRouter/OpenRouterProviderSettings.cs @@ -20,9 +20,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using SmartHopper.Infrastructure.AIProviders; -using SmartHopper.Infrastructure.Dialogs; -using SmartHopper.Infrastructure.Settings; +using SmartHopper.ProviderSdk.AIProviders; +using SmartHopper.ProviderSdk.Diagnostics; +using SmartHopper.ProviderSdk.Hosting; +using SmartHopper.ProviderSdk.Settings; namespace SmartHopper.Providers.OpenRouter { @@ -171,7 +172,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Max Tokens must be greater than 0.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Max Tokens must be greater than 0.")); } return false; @@ -190,7 +191,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Temperature must be between 0.0 and 2.0.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Temperature must be between 0.0 and 2.0.")); } return false; @@ -215,7 +216,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Sort must be one of: price, throughput, latency.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Sort must be one of: price, throughput, latency.")); } return false; @@ -231,7 +232,7 @@ public override bool ValidateSettings(Dictionary settings) { if (showErrorDialogs) { - StyledMessageDialog.ShowError("Data Collection must be either 'deny' or 'allow'.", "Validation Error"); + ProviderSdkHost.Diagnostics.Report(this.GetType().Name, new SHRuntimeMessage(SHRuntimeMessageSeverity.Error, SHRuntimeMessageOrigin.Validation, SHMessageCode.InputInvalid, "Data Collection must be either 'deny' or 'allow'.")); } return false; diff --git a/src/SmartHopper.Providers.OpenRouter/SmartHopper.Providers.OpenRouter.csproj b/src/SmartHopper.Providers.OpenRouter/SmartHopper.Providers.OpenRouter.csproj index 70bf58c66..8074c0d4f 100644 --- a/src/SmartHopper.Providers.OpenRouter/SmartHopper.Providers.OpenRouter.csproj +++ b/src/SmartHopper.Providers.OpenRouter/SmartHopper.Providers.OpenRouter.csproj @@ -35,7 +35,7 @@ - + diff --git a/tools/Update-ProviderModels.ps1 b/tools/Update-ProviderModels.ps1 index 80acbe0ae..95d66d184 100644 --- a/tools/Update-ProviderModels.ps1 +++ b/tools/Update-ProviderModels.ps1 @@ -325,6 +325,8 @@ function ConvertFrom-ModelBlock($blockText) { Deprecated = $null SupportsStreaming = $null SupportsPromptCaching = $null + SupportsBatch = $null + BatchPricing = $null Rank = $null ContextLimit = $null Created = $null @@ -343,6 +345,7 @@ function ConvertFrom-ModelBlock($blockText) { Deprecated = 'Deprecated\s*=\s*(true|false)' SupportsStreaming = 'SupportsStreaming\s*=\s*(true|false)' SupportsPromptCaching = 'SupportsPromptCaching\s*=\s*(true|false)' + SupportsBatch = 'SupportsBatch\s*=\s*(true|false)' Rank = 'Rank\s*=\s*(\d+)' ContextLimit = 'ContextLimit\s*=\s*(\d+)' CacheKeyStrategy = 'CacheKeyStrategy\s*=\s*"([^"]*)"' @@ -378,6 +381,17 @@ function ConvertFrom-ModelBlock($blockText) { if ($pricing.Count -gt 0) { $result.Pricing = [pscustomobject]$pricing } } + # BatchPricing = new AIModelPricing { ... } + $batchPricingRx = [regex]::Match($blockText, 'BatchPricing\s*=\s*new\s+AIModelPricing\s*\{\s*([^}]*)\s*\}') + if ($batchPricingRx.Success) { + $batchPriceBody = $batchPricingRx.Groups[1].Value + $batchPricing = [ordered]@{} + foreach ($pm in [regex]::Matches($batchPriceBody, '(\w+)\s*=\s*([0-9]*\.?[0-9]+(?:[eE][-+]?\d+)?)m?')) { + $batchPricing[$pm.Groups[1].Value] = $pm.Groups[2].Value + } + if ($batchPricing.Count -gt 0) { $result.BatchPricing = [pscustomobject]$batchPricing } + } + # Aliases $aliasesRx = [regex]::Match($blockText, 'Aliases\s*=\s*new\s+List\s*\{\s*([^}]*)\s*\}') if ($aliasesRx.Success) { @@ -418,6 +432,9 @@ function Format-ModelBlock($model, $providerVar) { if ($model.SupportsPromptCaching -eq 'true') { $lines.Add(' SupportsPromptCaching = true,') } if ($model.SupportsPromptCaching -eq 'false') { $lines.Add(' SupportsPromptCaching = false,') } + if ($model.SupportsBatch -eq 'true') { $lines.Add(' SupportsBatch = true,') } + if ($model.SupportsBatch -eq 'false') { $lines.Add(' SupportsBatch = false,') } + if ($model.Verified -eq 'true') { $lines.Add(' Verified = true,') } if ($model.Verified -eq 'false') { $lines.Add(' Verified = false,') } @@ -461,6 +478,24 @@ function Format-ModelBlock($model, $providerVar) { } } + if ($model.BatchPricing) { + $batchPriceProps = [System.Collections.Generic.List[string]]::new() + foreach ($p in @('Prompt','Completion','Request','Image','ImageOutput','ImageToken','Audio','AudioOutput','InputAudioCache','InputCacheRead','InputCacheWrite','InternalReasoning','WebSearch','Discount')) { + $v = $model.BatchPricing.$p + if ($null -ne $v -and -not [string]::IsNullOrWhiteSpace([string]$v)) { + $num = [string]$v + $num = $num.TrimEnd('m','M') + [void]$batchPriceProps.Add(" $p = ${num}m,") + } + } + if ($batchPriceProps.Count -gt 0) { + $lines.Add(' BatchPricing = new AIModelPricing') + $lines.Add(' {') + foreach ($pp in $batchPriceProps) { $lines.Add($pp) } + $lines.Add(' },') + } + } + if ($model.Aliases -and $model.Aliases.Count -gt 0) { $aliasParts = $model.Aliases | ForEach-Object { "`"$_`"" } $lines.Add(" Aliases = new List { $($aliasParts -join ', ') },") @@ -753,7 +788,10 @@ if ($ValidateOnly) { # --------------------------------------------------------------------------- # 2. Query OpenRouter # --------------------------------------------------------------------------- -$headers = @{ Authorization = "Bearer $OpenRouterApiKey" } +$headers = @{} +if (-not [string]::IsNullOrWhiteSpace($OpenRouterApiKey)) { + $headers['Authorization'] = "Bearer $OpenRouterApiKey" +} try { $response = Invoke-RestMethod -Uri $OpenRouterUrl -Headers $headers -Method GET -TimeoutSec 60 @@ -787,7 +825,7 @@ function Get-LogicalModelKey($modelName) { return ([string]$modelName).ToLowerInvariant() -replace '(?<=\d)[\.-](?=\d)', '.' } -$openRouterModels = [System.Collections.Generic.List[psobject]]::new() +$rawOpenRouterModels = [System.Collections.Generic.List[psobject]]::new() foreach ($item in $response.data) { $fullId = $item.id if ([string]::IsNullOrWhiteSpace($fullId)) { continue } @@ -797,17 +835,48 @@ foreach ($item in $response.data) { if ($isOpenRouterProvider) { # OpenRouter provider: keep every model verbatim (full "vendor/model" id). - $openRouterModels.Add($item) + $rawOpenRouterModels.Add($item) } elseif ($fullId.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { # Strip provider prefix: "openai/gpt-4o" -> "gpt-4o". if (-not [string]::IsNullOrWhiteSpace($fullId.Substring($prefix.Length))) { - $openRouterModels.Add($item) + $rawOpenRouterModels.Add($item) + } + } +} + +# Separate :batch variants. They are not standalone chat models; they are used +# to flag batch support and capture discounted BatchPricing on the base model. +$batchVariantsByBase = [ordered]@{} +$openRouterModels = [System.Collections.Generic.List[psobject]]::new() +foreach ($orm in $rawOpenRouterModels) { + $fullId = $orm.id + if ($fullId -match ':batch$') { + $baseKey = if ($isOpenRouterProvider) { $fullId -replace ':batch$','' } else { Get-ModelName $fullId } + $batchVariantsByBase[$baseKey] = $orm + } + else { + $openRouterModels.Add($orm) + } +} + +# For OpenRouter, a :batch variant may appear without its base. Synthesize a +# base entry from the batch variant so the model is not dropped. +if ($isOpenRouterProvider) { + $knownBaseIds = [System.Collections.Generic.HashSet[string]]::new( + [string[]]($openRouterModels | ForEach-Object { $_.id }), + [System.StringComparer]::OrdinalIgnoreCase) + foreach ($kvp in $batchVariantsByBase.GetEnumerator()) { + $baseKey = $kvp.Key + if (-not $knownBaseIds.Contains($baseKey)) { + $synthetic = $kvp.Value | Select-Object * + $synthetic.id = $baseKey + $openRouterModels.Add($synthetic) } } } -Write-Host "[$Provider] OpenRouter returned $($openRouterModels.Count) model(s) for prefix '$prefix'." +Write-Host "[$Provider] OpenRouter returned $($openRouterModels.Count) base model(s) for prefix '$prefix' ($($batchVariantsByBase.Count) batch variant(s) merged into base models)." # For non-OpenRouter providers, deduplicate models that differ only by # OpenRouter pricing-tier suffix (e.g. "model:free" vs "model"). @@ -1481,6 +1550,46 @@ else { } } +# --------------------------------------------------------------------------- +# Merge :batch variant metadata into the base model. +# +# OpenRouter returns :batch variants as separate models. They are not standalone +# chat models; they are used to flag batch support and to capture the discounted +# BatchPricing on the base model. The original :batch slug is kept as an alias. +# --------------------------------------------------------------------------- +foreach ($kvp in $batchVariantsByBase.GetEnumerator()) { + $baseKey = $kvp.Key + $batchVariant = $kvp.Value + if (-not $mergedModels.Contains($baseKey)) { continue } + + $model = $mergedModels[$baseKey] + $batchEnrichment = Get-OpenRouterEnrichment -orm $batchVariant + if ($batchEnrichment -and $batchEnrichment.Pricing) { + $model | Add-Member -NotePropertyName 'BatchPricing' -NotePropertyValue $batchEnrichment.Pricing -Force + } + $model | Add-Member -NotePropertyName 'SupportsBatch' -NotePropertyValue 'true' -Force + + $batchAlias = if ($isOpenRouterProvider) { $batchVariant.id } else { $batchVariant.id.Substring($prefix.Length) } + if (-not [string]::IsNullOrWhiteSpace($batchAlias)) { + $aliases = [System.Collections.Generic.List[string]]::new() + if ($model.Aliases) { foreach ($a in @($model.Aliases)) { [void]$aliases.Add($a) } } + $already = $aliases | Where-Object { [string]::Equals($_, $batchAlias, 'OrdinalIgnoreCase') } | Select-Object -First 1 + if (-not $already) { [void]$aliases.Add($batchAlias) } + $model.Aliases = $aliases.ToArray() + } +} + +# For the OpenRouter provider, any base model without a :batch variant is +# explicitly not batch-compatible. Other providers leave SupportsBatch as null +# (unknown/try) unless a :batch variant was found above. +if ($isOpenRouterProvider) { + foreach ($m in $mergedModels.Values) { + if ($m.SupportsBatch -ne 'true') { + $m | Add-Member -NotePropertyName 'SupportsBatch' -NotePropertyValue 'false' -Force + } + } +} + # --------------------------------------------------------------------------- # Normalize Aliases: remove self-references and duplicates (case-insensitive). # --------------------------------------------------------------------------- diff --git a/yak-package/manifest.yml b/yak-package/manifest.yml index 7d52d3ea7..0d51bd2a5 100644 --- a/yak-package/manifest.yml +++ b/yak-package/manifest.yml @@ -49,4 +49,6 @@ keywords: - anthropic - open-router - gemini + - localai + - ollama icon: icon.png