Merge branch 'dev' into abhi/integration-test-setup

This commit is contained in:
Abhimanyu Yadav
2026-01-21 21:26:09 +05:30
committed by GitHub
30 changed files with 1073 additions and 155 deletions

View File

@@ -79,6 +79,10 @@ class ModelMetadata(NamedTuple):
provider: str
context_window: int
max_output_tokens: int | None
display_name: str
provider_name: str
creator_name: str
price_tier: Literal[1, 2, 3]
class LlmModelMeta(EnumMeta):
@@ -171,6 +175,26 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
V0_1_5_LG = "v0-1.5-lg"
V0_1_0_MD = "v0-1.0-md"
@classmethod
def __get_pydantic_json_schema__(cls, schema, handler):
json_schema = handler(schema)
llm_model_metadata = {}
for model in cls:
model_name = model.value
metadata = model.metadata
llm_model_metadata[model_name] = {
"creator": metadata.creator_name,
"creator_name": metadata.creator_name,
"title": metadata.display_name,
"provider": metadata.provider,
"provider_name": metadata.provider_name,
"name": model_name,
"price_tier": metadata.price_tier,
}
json_schema["llm_model"] = True
json_schema["llm_model_metadata"] = llm_model_metadata
return json_schema
@property
def metadata(self) -> ModelMetadata:
return MODEL_METADATA[self]
@@ -190,119 +214,291 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
MODEL_METADATA = {
# https://platform.openai.com/docs/models
LlmModel.O3: ModelMetadata("openai", 200000, 100000),
LlmModel.O3_MINI: ModelMetadata("openai", 200000, 100000), # o3-mini-2025-01-31
LlmModel.O1: ModelMetadata("openai", 200000, 100000), # o1-2024-12-17
LlmModel.O1_MINI: ModelMetadata("openai", 128000, 65536), # o1-mini-2024-09-12
LlmModel.O3: ModelMetadata("openai", 200000, 100000, "O3", "OpenAI", "OpenAI", 2),
LlmModel.O3_MINI: ModelMetadata(
"openai", 200000, 100000, "O3 Mini", "OpenAI", "OpenAI", 1
), # o3-mini-2025-01-31
LlmModel.O1: ModelMetadata(
"openai", 200000, 100000, "O1", "OpenAI", "OpenAI", 3
), # o1-2024-12-17
LlmModel.O1_MINI: ModelMetadata(
"openai", 128000, 65536, "O1 Mini", "OpenAI", "OpenAI", 2
), # o1-mini-2024-09-12
# GPT-5 models
LlmModel.GPT5_2: ModelMetadata("openai", 400000, 128000),
LlmModel.GPT5_1: ModelMetadata("openai", 400000, 128000),
LlmModel.GPT5: ModelMetadata("openai", 400000, 128000),
LlmModel.GPT5_MINI: ModelMetadata("openai", 400000, 128000),
LlmModel.GPT5_NANO: ModelMetadata("openai", 400000, 128000),
LlmModel.GPT5_CHAT: ModelMetadata("openai", 400000, 16384),
LlmModel.GPT41: ModelMetadata("openai", 1047576, 32768),
LlmModel.GPT41_MINI: ModelMetadata("openai", 1047576, 32768),
LlmModel.GPT5_2: ModelMetadata(
"openai", 400000, 128000, "GPT-5.2", "OpenAI", "OpenAI", 3
),
LlmModel.GPT5_1: ModelMetadata(
"openai", 400000, 128000, "GPT-5.1", "OpenAI", "OpenAI", 2
),
LlmModel.GPT5: ModelMetadata(
"openai", 400000, 128000, "GPT-5", "OpenAI", "OpenAI", 1
),
LlmModel.GPT5_MINI: ModelMetadata(
"openai", 400000, 128000, "GPT-5 Mini", "OpenAI", "OpenAI", 1
),
LlmModel.GPT5_NANO: ModelMetadata(
"openai", 400000, 128000, "GPT-5 Nano", "OpenAI", "OpenAI", 1
),
LlmModel.GPT5_CHAT: ModelMetadata(
"openai", 400000, 16384, "GPT-5 Chat Latest", "OpenAI", "OpenAI", 2
),
LlmModel.GPT41: ModelMetadata(
"openai", 1047576, 32768, "GPT-4.1", "OpenAI", "OpenAI", 1
),
LlmModel.GPT41_MINI: ModelMetadata(
"openai", 1047576, 32768, "GPT-4.1 Mini", "OpenAI", "OpenAI", 1
),
LlmModel.GPT4O_MINI: ModelMetadata(
"openai", 128000, 16384
"openai", 128000, 16384, "GPT-4o Mini", "OpenAI", "OpenAI", 1
), # gpt-4o-mini-2024-07-18
LlmModel.GPT4O: ModelMetadata("openai", 128000, 16384), # gpt-4o-2024-08-06
LlmModel.GPT4O: ModelMetadata(
"openai", 128000, 16384, "GPT-4o", "OpenAI", "OpenAI", 2
), # gpt-4o-2024-08-06
LlmModel.GPT4_TURBO: ModelMetadata(
"openai", 128000, 4096
"openai", 128000, 4096, "GPT-4 Turbo", "OpenAI", "OpenAI", 3
), # gpt-4-turbo-2024-04-09
LlmModel.GPT3_5_TURBO: ModelMetadata("openai", 16385, 4096), # gpt-3.5-turbo-0125
LlmModel.GPT3_5_TURBO: ModelMetadata(
"openai", 16385, 4096, "GPT-3.5 Turbo", "OpenAI", "OpenAI", 1
), # gpt-3.5-turbo-0125
# https://docs.anthropic.com/en/docs/about-claude/models
LlmModel.CLAUDE_4_1_OPUS: ModelMetadata(
"anthropic", 200000, 32000
"anthropic", 200000, 32000, "Claude Opus 4.1", "Anthropic", "Anthropic", 3
), # claude-opus-4-1-20250805
LlmModel.CLAUDE_4_OPUS: ModelMetadata(
"anthropic", 200000, 32000
"anthropic", 200000, 32000, "Claude Opus 4", "Anthropic", "Anthropic", 3
), # claude-4-opus-20250514
LlmModel.CLAUDE_4_SONNET: ModelMetadata(
"anthropic", 200000, 64000
"anthropic", 200000, 64000, "Claude Sonnet 4", "Anthropic", "Anthropic", 2
), # claude-4-sonnet-20250514
LlmModel.CLAUDE_4_5_OPUS: ModelMetadata(
"anthropic", 200000, 64000
"anthropic", 200000, 64000, "Claude Opus 4.5", "Anthropic", "Anthropic", 3
), # claude-opus-4-5-20251101
LlmModel.CLAUDE_4_5_SONNET: ModelMetadata(
"anthropic", 200000, 64000
"anthropic", 200000, 64000, "Claude Sonnet 4.5", "Anthropic", "Anthropic", 3
), # claude-sonnet-4-5-20250929
LlmModel.CLAUDE_4_5_HAIKU: ModelMetadata(
"anthropic", 200000, 64000
"anthropic", 200000, 64000, "Claude Haiku 4.5", "Anthropic", "Anthropic", 2
), # claude-haiku-4-5-20251001
LlmModel.CLAUDE_3_7_SONNET: ModelMetadata(
"anthropic", 200000, 64000
"anthropic", 200000, 64000, "Claude 3.7 Sonnet", "Anthropic", "Anthropic", 2
), # claude-3-7-sonnet-20250219
LlmModel.CLAUDE_3_HAIKU: ModelMetadata(
"anthropic", 200000, 4096
"anthropic", 200000, 4096, "Claude 3 Haiku", "Anthropic", "Anthropic", 1
), # claude-3-haiku-20240307
# https://docs.aimlapi.com/api-overview/model-database/text-models
LlmModel.AIML_API_QWEN2_5_72B: ModelMetadata("aiml_api", 32000, 8000),
LlmModel.AIML_API_LLAMA3_1_70B: ModelMetadata("aiml_api", 128000, 40000),
LlmModel.AIML_API_LLAMA3_3_70B: ModelMetadata("aiml_api", 128000, None),
LlmModel.AIML_API_META_LLAMA_3_1_70B: ModelMetadata("aiml_api", 131000, 2000),
LlmModel.AIML_API_LLAMA_3_2_3B: ModelMetadata("aiml_api", 128000, None),
# https://console.groq.com/docs/models
LlmModel.LLAMA3_3_70B: ModelMetadata("groq", 128000, 32768),
LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 128000, 8192),
# https://ollama.com/library
LlmModel.OLLAMA_LLAMA3_3: ModelMetadata("ollama", 8192, None),
LlmModel.OLLAMA_LLAMA3_2: ModelMetadata("ollama", 8192, None),
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192, None),
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192, None),
LlmModel.OLLAMA_DOLPHIN: ModelMetadata("ollama", 32768, None),
# https://openrouter.ai/models
LlmModel.GEMINI_2_5_PRO: ModelMetadata("open_router", 1050000, 8192),
LlmModel.GEMINI_3_PRO_PREVIEW: ModelMetadata("open_router", 1048576, 65535),
LlmModel.GEMINI_2_5_FLASH: ModelMetadata("open_router", 1048576, 65535),
LlmModel.GEMINI_2_0_FLASH: ModelMetadata("open_router", 1048576, 8192),
LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: ModelMetadata(
"open_router", 1048576, 65535
LlmModel.AIML_API_QWEN2_5_72B: ModelMetadata(
"aiml_api", 32000, 8000, "Qwen 2.5 72B Instruct Turbo", "AI/ML", "Qwen", 1
),
LlmModel.AIML_API_LLAMA3_1_70B: ModelMetadata(
"aiml_api",
128000,
40000,
"Llama 3.1 Nemotron 70B Instruct",
"AI/ML",
"Nvidia",
1,
),
LlmModel.AIML_API_LLAMA3_3_70B: ModelMetadata(
"aiml_api", 128000, None, "Llama 3.3 70B Instruct Turbo", "AI/ML", "Meta", 1
),
LlmModel.AIML_API_META_LLAMA_3_1_70B: ModelMetadata(
"aiml_api", 131000, 2000, "Llama 3.1 70B Instruct Turbo", "AI/ML", "Meta", 1
),
LlmModel.AIML_API_LLAMA_3_2_3B: ModelMetadata(
"aiml_api", 128000, None, "Llama 3.2 3B Instruct Turbo", "AI/ML", "Meta", 1
),
# https://console.groq.com/docs/models
LlmModel.LLAMA3_3_70B: ModelMetadata(
"groq", 128000, 32768, "Llama 3.3 70B Versatile", "Groq", "Meta", 1
),
LlmModel.LLAMA3_1_8B: ModelMetadata(
"groq", 128000, 8192, "Llama 3.1 8B Instant", "Groq", "Meta", 1
),
# https://ollama.com/library
LlmModel.OLLAMA_LLAMA3_3: ModelMetadata(
"ollama", 8192, None, "Llama 3.3", "Ollama", "Meta", 1
),
LlmModel.OLLAMA_LLAMA3_2: ModelMetadata(
"ollama", 8192, None, "Llama 3.2", "Ollama", "Meta", 1
),
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata(
"ollama", 8192, None, "Llama 3", "Ollama", "Meta", 1
),
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata(
"ollama", 8192, None, "Llama 3.1 405B", "Ollama", "Meta", 1
),
LlmModel.OLLAMA_DOLPHIN: ModelMetadata(
"ollama", 32768, None, "Dolphin Mistral Latest", "Ollama", "Mistral AI", 1
),
# https://openrouter.ai/models
LlmModel.GEMINI_2_5_PRO: ModelMetadata(
"open_router",
1050000,
8192,
"Gemini 2.5 Pro Preview 03.25",
"OpenRouter",
"Google",
2,
),
LlmModel.GEMINI_3_PRO_PREVIEW: ModelMetadata(
"open_router", 1048576, 65535, "Gemini 3 Pro Preview", "OpenRouter", "Google", 2
),
LlmModel.GEMINI_2_5_FLASH: ModelMetadata(
"open_router", 1048576, 65535, "Gemini 2.5 Flash", "OpenRouter", "Google", 1
),
LlmModel.GEMINI_2_0_FLASH: ModelMetadata(
"open_router", 1048576, 8192, "Gemini 2.0 Flash 001", "OpenRouter", "Google", 1
),
LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: ModelMetadata(
"open_router",
1048576,
65535,
"Gemini 2.5 Flash Lite Preview 06.17",
"OpenRouter",
"Google",
1,
),
LlmModel.GEMINI_2_0_FLASH_LITE: ModelMetadata(
"open_router",
1048576,
8192,
"Gemini 2.0 Flash Lite 001",
"OpenRouter",
"Google",
1,
),
LlmModel.MISTRAL_NEMO: ModelMetadata(
"open_router", 128000, 4096, "Mistral Nemo", "OpenRouter", "Mistral AI", 1
),
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata(
"open_router", 128000, 4096, "Command R 08.2024", "OpenRouter", "Cohere", 1
),
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata(
"open_router", 128000, 4096, "Command R Plus 08.2024", "OpenRouter", "Cohere", 2
),
LlmModel.DEEPSEEK_CHAT: ModelMetadata(
"open_router", 64000, 2048, "DeepSeek Chat", "OpenRouter", "DeepSeek", 1
),
LlmModel.DEEPSEEK_R1_0528: ModelMetadata(
"open_router", 163840, 163840, "DeepSeek R1 0528", "OpenRouter", "DeepSeek", 1
),
LlmModel.PERPLEXITY_SONAR: ModelMetadata(
"open_router", 127000, 8000, "Sonar", "OpenRouter", "Perplexity", 1
),
LlmModel.PERPLEXITY_SONAR_PRO: ModelMetadata(
"open_router", 200000, 8000, "Sonar Pro", "OpenRouter", "Perplexity", 2
),
LlmModel.GEMINI_2_0_FLASH_LITE: ModelMetadata("open_router", 1048576, 8192),
LlmModel.MISTRAL_NEMO: ModelMetadata("open_router", 128000, 4096),
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata("open_router", 128000, 4096),
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata("open_router", 128000, 4096),
LlmModel.DEEPSEEK_CHAT: ModelMetadata("open_router", 64000, 2048),
LlmModel.DEEPSEEK_R1_0528: ModelMetadata("open_router", 163840, 163840),
LlmModel.PERPLEXITY_SONAR: ModelMetadata("open_router", 127000, 8000),
LlmModel.PERPLEXITY_SONAR_PRO: ModelMetadata("open_router", 200000, 8000),
LlmModel.PERPLEXITY_SONAR_DEEP_RESEARCH: ModelMetadata(
"open_router",
128000,
16000,
"Sonar Deep Research",
"OpenRouter",
"Perplexity",
3,
),
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B: ModelMetadata(
"open_router", 131000, 4096
"open_router",
131000,
4096,
"Hermes 3 Llama 3.1 405B",
"OpenRouter",
"Nous Research",
1,
),
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B: ModelMetadata(
"open_router", 12288, 12288
"open_router",
12288,
12288,
"Hermes 3 Llama 3.1 70B",
"OpenRouter",
"Nous Research",
1,
),
LlmModel.OPENAI_GPT_OSS_120B: ModelMetadata(
"open_router", 131072, 131072, "GPT-OSS 120B", "OpenRouter", "OpenAI", 1
),
LlmModel.OPENAI_GPT_OSS_20B: ModelMetadata(
"open_router", 131072, 32768, "GPT-OSS 20B", "OpenRouter", "OpenAI", 1
),
LlmModel.AMAZON_NOVA_LITE_V1: ModelMetadata(
"open_router", 300000, 5120, "Nova Lite V1", "OpenRouter", "Amazon", 1
),
LlmModel.AMAZON_NOVA_MICRO_V1: ModelMetadata(
"open_router", 128000, 5120, "Nova Micro V1", "OpenRouter", "Amazon", 1
),
LlmModel.AMAZON_NOVA_PRO_V1: ModelMetadata(
"open_router", 300000, 5120, "Nova Pro V1", "OpenRouter", "Amazon", 1
),
LlmModel.MICROSOFT_WIZARDLM_2_8X22B: ModelMetadata(
"open_router", 65536, 4096, "WizardLM 2 8x22B", "OpenRouter", "Microsoft", 1
),
LlmModel.GRYPHE_MYTHOMAX_L2_13B: ModelMetadata(
"open_router", 4096, 4096, "MythoMax L2 13B", "OpenRouter", "Gryphe", 1
),
LlmModel.META_LLAMA_4_SCOUT: ModelMetadata(
"open_router", 131072, 131072, "Llama 4 Scout", "OpenRouter", "Meta", 1
),
LlmModel.META_LLAMA_4_MAVERICK: ModelMetadata(
"open_router", 1048576, 1000000, "Llama 4 Maverick", "OpenRouter", "Meta", 1
),
LlmModel.GROK_4: ModelMetadata(
"open_router", 256000, 256000, "Grok 4", "OpenRouter", "xAI", 3
),
LlmModel.GROK_4_FAST: ModelMetadata(
"open_router", 2000000, 30000, "Grok 4 Fast", "OpenRouter", "xAI", 1
),
LlmModel.GROK_4_1_FAST: ModelMetadata(
"open_router", 2000000, 30000, "Grok 4.1 Fast", "OpenRouter", "xAI", 1
),
LlmModel.GROK_CODE_FAST_1: ModelMetadata(
"open_router", 256000, 10000, "Grok Code Fast 1", "OpenRouter", "xAI", 1
),
LlmModel.KIMI_K2: ModelMetadata(
"open_router", 131000, 131000, "Kimi K2", "OpenRouter", "Moonshot AI", 1
),
LlmModel.QWEN3_235B_A22B_THINKING: ModelMetadata(
"open_router",
262144,
262144,
"Qwen 3 235B A22B Thinking 2507",
"OpenRouter",
"Qwen",
1,
),
LlmModel.QWEN3_CODER: ModelMetadata(
"open_router", 262144, 262144, "Qwen 3 Coder", "OpenRouter", "Qwen", 3
),
LlmModel.OPENAI_GPT_OSS_120B: ModelMetadata("open_router", 131072, 131072),
LlmModel.OPENAI_GPT_OSS_20B: ModelMetadata("open_router", 131072, 32768),
LlmModel.AMAZON_NOVA_LITE_V1: ModelMetadata("open_router", 300000, 5120),
LlmModel.AMAZON_NOVA_MICRO_V1: ModelMetadata("open_router", 128000, 5120),
LlmModel.AMAZON_NOVA_PRO_V1: ModelMetadata("open_router", 300000, 5120),
LlmModel.MICROSOFT_WIZARDLM_2_8X22B: ModelMetadata("open_router", 65536, 4096),
LlmModel.GRYPHE_MYTHOMAX_L2_13B: ModelMetadata("open_router", 4096, 4096),
LlmModel.META_LLAMA_4_SCOUT: ModelMetadata("open_router", 131072, 131072),
LlmModel.META_LLAMA_4_MAVERICK: ModelMetadata("open_router", 1048576, 1000000),
LlmModel.GROK_4: ModelMetadata("open_router", 256000, 256000),
LlmModel.GROK_4_FAST: ModelMetadata("open_router", 2000000, 30000),
LlmModel.GROK_4_1_FAST: ModelMetadata("open_router", 2000000, 30000),
LlmModel.GROK_CODE_FAST_1: ModelMetadata("open_router", 256000, 10000),
LlmModel.KIMI_K2: ModelMetadata("open_router", 131000, 131000),
LlmModel.QWEN3_235B_A22B_THINKING: ModelMetadata("open_router", 262144, 262144),
LlmModel.QWEN3_CODER: ModelMetadata("open_router", 262144, 262144),
# Llama API models
LlmModel.LLAMA_API_LLAMA_4_SCOUT: ModelMetadata("llama_api", 128000, 4028),
LlmModel.LLAMA_API_LLAMA4_MAVERICK: ModelMetadata("llama_api", 128000, 4028),
LlmModel.LLAMA_API_LLAMA3_3_8B: ModelMetadata("llama_api", 128000, 4028),
LlmModel.LLAMA_API_LLAMA3_3_70B: ModelMetadata("llama_api", 128000, 4028),
LlmModel.LLAMA_API_LLAMA_4_SCOUT: ModelMetadata(
"llama_api",
128000,
4028,
"Llama 4 Scout 17B 16E Instruct FP8",
"Llama API",
"Meta",
1,
),
LlmModel.LLAMA_API_LLAMA4_MAVERICK: ModelMetadata(
"llama_api",
128000,
4028,
"Llama 4 Maverick 17B 128E Instruct FP8",
"Llama API",
"Meta",
1,
),
LlmModel.LLAMA_API_LLAMA3_3_8B: ModelMetadata(
"llama_api", 128000, 4028, "Llama 3.3 8B Instruct", "Llama API", "Meta", 1
),
LlmModel.LLAMA_API_LLAMA3_3_70B: ModelMetadata(
"llama_api", 128000, 4028, "Llama 3.3 70B Instruct", "Llama API", "Meta", 1
),
# v0 by Vercel models
LlmModel.V0_1_5_MD: ModelMetadata("v0", 128000, 64000),
LlmModel.V0_1_5_LG: ModelMetadata("v0", 512000, 64000),
LlmModel.V0_1_0_MD: ModelMetadata("v0", 128000, 64000),
LlmModel.V0_1_5_MD: ModelMetadata("v0", 128000, 64000, "v0 1.5 MD", "V0", "V0", 1),
LlmModel.V0_1_5_LG: ModelMetadata("v0", 512000, 64000, "v0 1.5 LG", "V0", "V0", 1),
LlmModel.V0_1_0_MD: ModelMetadata("v0", 128000, 64000, "v0 1.0 MD", "V0", "V0", 1),
}
DEFAULT_LLM_MODEL = LlmModel.GPT5_2

View File

@@ -99,10 +99,15 @@ MODEL_COST: dict[LlmModel, int] = {
LlmModel.OPENAI_GPT_OSS_20B: 1,
LlmModel.GEMINI_2_5_PRO: 4,
LlmModel.GEMINI_3_PRO_PREVIEW: 5,
LlmModel.GEMINI_2_5_FLASH: 1,
LlmModel.GEMINI_2_0_FLASH: 1,
LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: 1,
LlmModel.GEMINI_2_0_FLASH_LITE: 1,
LlmModel.MISTRAL_NEMO: 1,
LlmModel.COHERE_COMMAND_R_08_2024: 1,
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: 3,
LlmModel.DEEPSEEK_CHAT: 2,
LlmModel.DEEPSEEK_R1_0528: 1,
LlmModel.PERPLEXITY_SONAR: 1,
LlmModel.PERPLEXITY_SONAR_PRO: 5,
LlmModel.PERPLEXITY_SONAR_DEEP_RESEARCH: 10,
@@ -126,11 +131,6 @@ MODEL_COST: dict[LlmModel, int] = {
LlmModel.KIMI_K2: 1,
LlmModel.QWEN3_235B_A22B_THINKING: 1,
LlmModel.QWEN3_CODER: 9,
LlmModel.GEMINI_2_5_FLASH: 1,
LlmModel.GEMINI_2_0_FLASH: 1,
LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: 1,
LlmModel.GEMINI_2_0_FLASH_LITE: 1,
LlmModel.DEEPSEEK_R1_0528: 1,
# v0 by Vercel models
LlmModel.V0_1_5_MD: 1,
LlmModel.V0_1_5_LG: 2,

View File

@@ -3,7 +3,7 @@ import logging
import uuid
from collections import defaultdict
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, cast
from prisma.enums import SubmissionStatus
from prisma.models import (
@@ -20,7 +20,7 @@ from prisma.types import (
AgentNodeLinkCreateInput,
StoreListingVersionWhereInput,
)
from pydantic import BaseModel, Field, create_model
from pydantic import BaseModel, BeforeValidator, Field, create_model
from pydantic.fields import computed_field
from backend.blocks.agent import AgentExecutorBlock
@@ -62,8 +62,14 @@ logger = logging.getLogger(__name__)
class GraphSettings(BaseModel):
human_in_the_loop_safe_mode: bool = True
sensitive_action_safe_mode: bool = False
# Use Annotated with BeforeValidator to coerce None to default values.
# This handles cases where the database has null values for these fields.
human_in_the_loop_safe_mode: Annotated[
bool, BeforeValidator(lambda v: v if v is not None else True)
] = True
sensitive_action_safe_mode: Annotated[
bool, BeforeValidator(lambda v: v if v is not None else False)
] = False
@classmethod
def from_graph(

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,8 +1,15 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import {
CredentialsMetaInput,
CredentialsType,
GraphExecutionID,
GraphMeta,
LibraryAgentPreset,
@@ -29,7 +36,11 @@ import {
} from "@/components/__legacy__/ui/icons";
import { Input } from "@/components/__legacy__/ui/input";
import { Button } from "@/components/atoms/Button/Button";
import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
import {
findSavedCredentialByProviderAndType,
findSavedUserCredentialByProviderAndType,
} from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import {
useToast,
@@ -37,6 +48,7 @@ import {
} from "@/components/molecules/Toast/use-toast";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { cn, isEmpty } from "@/lib/utils";
import { CredentialsProvidersContext } from "@/providers/agent-credentials/credentials-provider";
import { ClockIcon, CopyIcon, InfoIcon } from "@phosphor-icons/react";
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
@@ -90,6 +102,7 @@ export function AgentRunDraftView({
const api = useBackendAPI();
const { toast } = useToast();
const toastOnFail = useToastOnFail();
const allProviders = useContext(CredentialsProvidersContext);
const [inputValues, setInputValues] = useState<Record<string, any>>({});
const [inputCredentials, setInputCredentials] = useState<
@@ -128,6 +141,77 @@ export function AgentRunDraftView({
() => graph.credentials_input_schema.properties,
[graph],
);
const credentialFields = useMemo(
function getCredentialFields() {
return Object.entries(agentCredentialsInputFields);
},
[agentCredentialsInputFields],
);
const requiredCredentials = useMemo(
function getRequiredCredentials() {
return new Set(
(graph.credentials_input_schema?.required as string[]) || [],
);
},
[graph.credentials_input_schema?.required],
);
useEffect(
function initializeDefaultCredentials() {
if (!allProviders) return;
if (!graph.credentials_input_schema?.properties) return;
if (requiredCredentials.size === 0) return;
setInputCredentials(function updateCredentials(currentCreds) {
const next = { ...currentCreds };
let didAdd = false;
for (const key of requiredCredentials) {
if (next[key]) continue;
const schema = graph.credentials_input_schema.properties[key];
if (!schema) continue;
const providerNames = schema.credentials_provider || [];
const credentialTypes = schema.credentials_types || [];
const requiredScopes = schema.credentials_scopes;
const userCredential = findSavedUserCredentialByProviderAndType(
providerNames,
credentialTypes,
requiredScopes,
allProviders,
);
const savedCredential =
userCredential ||
findSavedCredentialByProviderAndType(
providerNames,
credentialTypes,
requiredScopes,
allProviders,
);
if (!savedCredential) continue;
next[key] = {
id: savedCredential.id,
provider: savedCredential.provider,
type: savedCredential.type as CredentialsType,
title: savedCredential.title,
};
didAdd = true;
}
if (!didAdd) return currentCreds;
return next;
});
},
[
allProviders,
graph.credentials_input_schema?.properties,
requiredCredentials,
],
);
const [allRequiredInputsAreSet, missingInputs] = useMemo(() => {
const nonEmptyInputs = new Set(
@@ -145,18 +229,35 @@ export function AgentRunDraftView({
);
return [isSuperset, difference];
}, [agentInputSchema.required, inputValues]);
const [allCredentialsAreSet, missingCredentials] = useMemo(() => {
const availableCredentials = new Set(Object.keys(inputCredentials));
const allCredentials = new Set(Object.keys(agentCredentialsInputFields));
// Backwards-compatible implementation of isSupersetOf and difference
const isSuperset = Array.from(allCredentials).every((item) =>
availableCredentials.has(item),
);
const difference = Array.from(allCredentials).filter(
(item) => !availableCredentials.has(item),
);
return [isSuperset, difference];
}, [agentCredentialsInputFields, inputCredentials]);
const [allCredentialsAreSet, missingCredentials] = useMemo(
function getCredentialStatus() {
const missing = Array.from(requiredCredentials).filter((key) => {
const cred = inputCredentials[key];
return !cred || !cred.id;
});
return [missing.length === 0, missing];
},
[requiredCredentials, inputCredentials],
);
function addChangedCredentials(prev: Set<keyof LibraryAgentPresetUpdatable>) {
const next = new Set(prev);
next.add("credentials");
return next;
}
function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
setInputCredentials(function updateInputCredentials(currentCreds) {
const next = { ...currentCreds };
if (value === undefined) {
delete next[key];
return next;
}
next[key] = value;
return next;
});
setChangedPresetAttributes(addChangedCredentials);
}
const notifyMissingInputs = useCallback(
(needPresetName: boolean = true) => {
const allMissingFields = (
@@ -649,35 +750,6 @@ export function AgentRunDraftView({
</>
)}
{/* Credentials inputs */}
{Object.entries(agentCredentialsInputFields).map(
([key, inputSubSchema]) => (
<CredentialsInput
key={key}
schema={{ ...inputSubSchema, discriminator: undefined }}
selectedCredentials={
inputCredentials[key] ?? inputSubSchema.default
}
onSelectCredentials={(value) => {
setInputCredentials((obj) => {
const newObj = { ...obj };
if (value === undefined) {
delete newObj[key];
return newObj;
}
return {
...obj,
[key]: value,
};
});
setChangedPresetAttributes((prev) =>
prev.add("credentials"),
);
}}
/>
),
)}
{/* Regular inputs */}
{Object.entries(agentInputFields).map(([key, inputSubSchema]) => (
<RunAgentInputs
@@ -695,6 +767,17 @@ export function AgentRunDraftView({
data-testid={`agent-input-${key}`}
/>
))}
{/* Credentials inputs */}
{credentialFields.length > 0 && (
<CredentialsGroupedView
credentialFields={credentialFields}
requiredCredentials={requiredCredentials}
inputCredentials={inputCredentials}
inputValues={inputValues}
onCredentialChange={handleCredentialChange}
/>
)}
</CardContent>
</Card>
</div>

View File

@@ -1,5 +1,5 @@
import { CredentialsProvidersContextType } from "@/providers/agent-credentials/credentials-provider";
import { getSystemCredentials } from "../../helpers";
import { filterSystemCredentials, getSystemCredentials } from "../../helpers";
export type CredentialField = [string, any];
@@ -208,3 +208,42 @@ export function findSavedCredentialByProviderAndType(
return undefined;
}
export function findSavedUserCredentialByProviderAndType(
providerNames: string[],
credentialTypes: string[],
requiredScopes: string[] | undefined,
allProviders: CredentialsProvidersContextType | null,
): SavedCredential | undefined {
for (const providerName of providerNames) {
const providerData = allProviders?.[providerName];
if (!providerData) continue;
const userCredentials = filterSystemCredentials(
providerData.savedCredentials ?? [],
);
const matchingCredentials: SavedCredential[] = [];
for (const credential of userCredentials) {
const typeMatches =
credentialTypes.length === 0 ||
credentialTypes.includes(credential.type);
const scopesMatch = hasRequiredScopes(credential, requiredScopes);
if (!typeMatches) continue;
if (!scopesMatch) continue;
matchingCredentials.push(credential as SavedCredential);
}
if (matchingCredentials.length === 1) {
return matchingCredentials[0];
}
if (matchingCredentials.length > 1) {
return undefined;
}
}
return undefined;
}

View File

@@ -98,24 +98,20 @@ export function useCredentialsInput({
// Auto-select the first available credential on initial mount
// Once a user has made a selection, we don't override it
useEffect(() => {
if (readOnly) return;
if (!credentials || !("savedCredentials" in credentials)) return;
useEffect(
function autoSelectCredential() {
if (readOnly) return;
if (!credentials || !("savedCredentials" in credentials)) return;
if (selectedCredential?.id) return;
// If already selected, don't auto-select
if (selectedCredential?.id) return;
const savedCreds = credentials.savedCredentials;
if (savedCreds.length === 0) return;
// Only attempt auto-selection once
if (hasAttemptedAutoSelect.current) return;
hasAttemptedAutoSelect.current = true;
if (hasAttemptedAutoSelect.current) return;
hasAttemptedAutoSelect.current = true;
// If optional, don't auto-select (user can choose "None")
if (isOptional) return;
if (isOptional) return;
const savedCreds = credentials.savedCredentials;
// Auto-select the first credential if any are available
if (savedCreds.length > 0) {
const cred = savedCreds[0];
onSelectCredential({
id: cred.id,
@@ -123,14 +119,15 @@ export function useCredentialsInput({
provider: credentials.provider,
title: (cred as any).title,
});
}
}, [
credentials,
selectedCredential?.id,
readOnly,
isOptional,
onSelectCredential,
]);
},
[
credentials,
selectedCredential?.id,
readOnly,
isOptional,
onSelectCredential,
],
);
if (
!credentials ||

View File

@@ -0,0 +1,33 @@
"use client";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-lg border border-zinc-200 bg-white p-4 text-zinc-900 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };

View File

@@ -0,0 +1,92 @@
"use client";
import {
descriptionId,
FieldProps,
getTemplate,
RJSFSchema,
titleId,
} from "@rjsf/utils";
import { useMemo } from "react";
import { LlmModelPicker } from "./components/LlmModelPicker";
import { LlmModelMetadataMap } from "./types";
import { updateUiOption } from "../../helpers";
type LlmModelSchema = RJSFSchema & {
llm_model_metadata?: LlmModelMetadataMap;
};
export function LlmModelField(props: FieldProps) {
const { schema, formData, onChange, disabled, readonly, fieldPathId } = props;
const metadata = useMemo(() => {
return (schema as LlmModelSchema)?.llm_model_metadata ?? {};
}, [schema]);
const models = useMemo(() => {
return Object.values(metadata);
}, [metadata]);
const selectedName =
typeof formData === "string"
? formData
: typeof schema.default === "string"
? schema.default
: "";
const selectedModel = selectedName
? (metadata[selectedName] ??
models.find((model) => model.name === selectedName))
: undefined;
const recommendedName =
typeof schema.default === "string" ? schema.default : models[0]?.name;
const recommendedModel =
recommendedName && metadata[recommendedName]
? metadata[recommendedName]
: undefined;
if (models.length === 0) {
return null;
}
const TitleFieldTemplate = getTemplate("TitleFieldTemplate", props.registry);
const DescriptionFieldTemplate = getTemplate(
"DescriptionFieldTemplate",
props.registry,
);
const updatedUiSchema = updateUiOption(props.uiSchema, {
showHandles: false,
});
return (
<>
<div className="flex items-center gap-2">
<TitleFieldTemplate
id={titleId(fieldPathId)}
title={schema.title || ""}
required={true}
schema={schema}
uiSchema={updatedUiSchema}
registry={props.registry}
/>
<DescriptionFieldTemplate
id={descriptionId(fieldPathId)}
description={schema.description || ""}
schema={schema}
registry={props.registry}
/>
</div>
<LlmModelPicker
models={models}
selectedModel={selectedModel}
recommendedModel={recommendedModel}
onSelect={(value) => onChange(value, fieldPathId?.path)}
disabled={disabled || readonly}
/>
</>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
import Image from "next/image";
import { Text } from "@/components/atoms/Text/Text";
const creatorIconMap: Record<string, string> = {
anthropic: "/integrations/anthropic-color.png",
openai: "/integrations/openai.png",
google: "/integrations/gemini.png",
nvidia: "/integrations/nvidia.png",
groq: "/integrations/groq.png",
ollama: "/integrations/ollama.png",
openrouter: "/integrations/open_router.png",
v0: "/integrations/v0.png",
xai: "/integrations/xai.webp",
meta: "/integrations/llama_api.png",
amazon: "/integrations/amazon.png",
cohere: "/integrations/cohere.png",
deepseek: "/integrations/deepseek.png",
gryphe: "/integrations/gryphe.png",
microsoft: "/integrations/microsoft.webp",
moonshotai: "/integrations/moonshot.png",
mistral: "/integrations/mistral.png",
mistralai: "/integrations/mistral.png",
nousresearch: "/integrations/nousresearch.avif",
perplexity: "/integrations/perplexity.webp",
qwen: "/integrations/qwen.png",
};
type Props = {
value: string;
size?: number;
};
export function LlmIcon({ value, size = 20 }: Props) {
const normalized = value.trim().toLowerCase().replace(/\s+/g, "");
const src = creatorIconMap[normalized];
if (src) {
return (
<div
className="flex items-center justify-center overflow-hidden rounded-xsmall"
style={{ width: size, height: size }}
>
<Image
src={src}
alt={value}
width={size}
height={size}
className="h-full w-full object-cover"
/>
</div>
);
}
const fallback = value?.trim().slice(0, 1).toUpperCase() || "?";
return (
<div
className="flex items-center justify-center rounded-xsmall bg-zinc-100"
style={{ width: size, height: size }}
>
<Text variant="small" className="text-zinc-500">
{fallback}
</Text>
</div>
);
}

View File

@@ -0,0 +1,24 @@
"use client";
import { ArrowLeftIcon } from "@phosphor-icons/react";
import { Text } from "@/components/atoms/Text/Text";
type Props = {
label: string;
onBack: () => void;
};
export function LlmMenuHeader({ label, onBack }: Props) {
return (
<button
type="button"
onClick={onBack}
className="flex w-full items-center gap-2 px-2 py-2 text-left hover:bg-zinc-100"
>
<ArrowLeftIcon className="h-4 w-4 text-zinc-800" weight="bold" />
<Text variant="body" className="text-zinc-900">
{label}
</Text>
</button>
);
}

View File

@@ -0,0 +1,61 @@
"use client";
import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
type Props = {
title: string;
subtitle?: string;
icon?: React.ReactNode;
showChevron?: boolean;
rightSlot?: React.ReactNode;
onClick: () => void;
isActive?: boolean;
};
export function LlmMenuItem({
title,
subtitle,
icon,
showChevron,
rightSlot,
onClick,
isActive,
}: Props) {
const hasIcon = Boolean(icon);
return (
<button
type="button"
onClick={onClick}
className={cn("w-full py-1 pl-2 pr-4 text-left hover:bg-zinc-100")}
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
{icon}
<Text variant="body" className="text-zinc-900">
{title}
</Text>
</div>
<div className="flex items-center gap-2">
{isActive && (
<CheckIcon className="h-4 w-4 text-emerald-600" weight="bold" />
)}
{rightSlot}
{showChevron && (
<CaretRightIcon className="h-4 w-4 text-zinc-900" weight="bold" />
)}
</div>
</div>
{subtitle && (
<Text
variant="small"
className={cn("mb-1 text-zinc-500", hasIcon && "pl-0")}
>
{subtitle}
</Text>
)}
</button>
);
}

View File

@@ -0,0 +1,235 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { CaretDownIcon } from "@phosphor-icons/react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/molecules/Popover/Popover";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import {
getCreatorDisplayName,
getModelDisplayName,
getProviderDisplayName,
groupByCreator,
groupByTitle,
} from "../helpers";
import { LlmModelMetadata } from "../types";
import { LlmIcon } from "./LlmIcon";
import { LlmMenuHeader } from "./LlmMenuHeader";
import { LlmMenuItem } from "./LlmMenuItem";
import { LlmPriceTier } from "./LlmPriceTier";
type MenuView = "creator" | "model" | "provider";
type Props = {
models: LlmModelMetadata[];
selectedModel?: LlmModelMetadata;
recommendedModel?: LlmModelMetadata;
onSelect: (value: string) => void;
disabled?: boolean;
};
export function LlmModelPicker({
models,
selectedModel,
recommendedModel,
onSelect,
disabled,
}: Props) {
const [open, setOpen] = useState(false);
const [view, setView] = useState<MenuView>("creator");
const [activeCreator, setActiveCreator] = useState<string | null>(null);
const [activeTitle, setActiveTitle] = useState<string | null>(null);
const modelsByCreator = useMemo(() => groupByCreator(models), [models]);
const creators = useMemo(() => {
return Array.from(modelsByCreator.keys()).sort((a, b) =>
a.localeCompare(b),
);
}, [modelsByCreator]);
const creatorIconValues = useMemo(() => {
const map = new Map<string, string>();
for (const [creator, entries] of modelsByCreator.entries()) {
map.set(creator, entries[0]?.creator ?? creator);
}
return map;
}, [modelsByCreator]);
useEffect(() => {
if (!open) {
return;
}
setView("creator");
setActiveCreator(
selectedModel
? getCreatorDisplayName(selectedModel)
: (creators[0] ?? null),
);
setActiveTitle(selectedModel ? getModelDisplayName(selectedModel) : null);
}, [open, selectedModel, creators]);
const currentCreator = activeCreator ?? creators[0] ?? null;
const currentModels = useMemo(() => {
return currentCreator ? (modelsByCreator.get(currentCreator) ?? []) : [];
}, [currentCreator, modelsByCreator]);
const currentCreatorIcon = useMemo(() => {
return currentModels[0]?.creator ?? currentCreator;
}, [currentModels, currentCreator]);
const modelsByTitle = useMemo(
() => groupByTitle(currentModels),
[currentModels],
);
const modelEntries = useMemo(() => {
return Array.from(modelsByTitle.entries())
.map(([title, entries]) => {
const providers = new Set(entries.map((entry) => entry.provider));
return {
title,
entries,
providerCount: providers.size,
};
})
.sort((a, b) => a.title.localeCompare(b.title));
}, [modelsByTitle]);
const providerEntries = useMemo(() => {
if (!activeTitle) {
return [];
}
return modelsByTitle.get(activeTitle) ?? [];
}, [activeTitle, modelsByTitle]);
const handleSelectModel = useCallback(
(modelName: string) => {
onSelect(modelName);
setOpen(false);
},
[onSelect],
);
const triggerModel = selectedModel ?? recommendedModel ?? models[0];
const triggerTitle = triggerModel
? getModelDisplayName(triggerModel)
: "Select model";
const triggerCreator = triggerModel?.creator ?? "";
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
disabled={disabled}
className={cn(
"flex w-full min-w-[15rem] items-center rounded-lg border border-zinc-200 bg-white px-3 py-2 text-left",
"hover:border-zinc-300 focus:outline-none focus:ring-2 focus:ring-zinc-200",
disabled && "cursor-not-allowed opacity-60",
)}
>
<LlmIcon value={triggerCreator} />
<Text variant="body" className="ml-1 flex-1 text-zinc-900">
{triggerTitle}
</Text>
<CaretDownIcon className="h-3 w-3 text-zinc-900" weight="bold" />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
sideOffset={4}
className="max-h-[45vh] w-[--radix-popover-trigger-width] min-w-[16rem] overflow-y-auto rounded-md border border-zinc-200 bg-white p-0 shadow-[0px_1px_4px_rgba(12,12,13,0.12)]"
>
{view === "creator" && (
<div className="flex flex-col">
{recommendedModel && (
<>
<LlmMenuItem
title={getModelDisplayName(recommendedModel)}
subtitle="Recommended"
icon={<LlmIcon value={recommendedModel.creator} />}
onClick={() => handleSelectModel(recommendedModel.name)}
/>
<div className="border-b border-zinc-200" />
</>
)}
{creators.map((creator) => (
<LlmMenuItem
key={creator}
title={creator}
icon={
<LlmIcon value={creatorIconValues.get(creator) ?? creator} />
}
showChevron={true}
isActive={
selectedModel
? getCreatorDisplayName(selectedModel) === creator
: false
}
onClick={() => {
setActiveCreator(creator);
setView("model");
}}
/>
))}
</div>
)}
{view === "model" && currentCreator && (
<div className="flex flex-col">
<LlmMenuHeader
label={currentCreator}
onBack={() => setView("creator")}
/>
<div className="border-b border-zinc-200" />
{modelEntries.map((entry) => (
<LlmMenuItem
key={entry.title}
title={entry.title}
icon={<LlmIcon value={currentCreatorIcon} />}
rightSlot={<LlmPriceTier tier={entry.entries[0]?.price_tier} />}
showChevron={entry.providerCount > 1}
isActive={
selectedModel
? getModelDisplayName(selectedModel) === entry.title
: false
}
onClick={() => {
if (entry.providerCount > 1) {
setActiveTitle(entry.title);
setView("provider");
return;
}
handleSelectModel(entry.entries[0].name);
}}
/>
))}
</div>
)}
{view === "provider" && activeTitle && (
<div className="flex flex-col">
<LlmMenuHeader
label={activeTitle}
onBack={() => setView("model")}
/>
<div className="border-b border-zinc-200" />
{providerEntries.map((entry) => (
<LlmMenuItem
key={`${entry.title}-${entry.provider}`}
title={getProviderDisplayName(entry)}
icon={<LlmIcon value={entry.provider} />}
isActive={selectedModel?.provider === entry.provider}
onClick={() => handleSelectModel(entry.name)}
/>
))}
</div>
)}
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,25 @@
"use client";
import { CurrencyDollarSimpleIcon } from "@phosphor-icons/react";
type Props = {
tier?: number;
};
export function LlmPriceTier({ tier }: Props) {
if (!tier || tier <= 0) {
return null;
}
const clamped = Math.min(3, Math.max(1, tier));
return (
<div className="flex items-center text-zinc-900">
{Array.from({ length: clamped }).map((_, index) => (
<CurrencyDollarSimpleIcon
key={`price-${index}`}
className="-mr-0.5 h-3 w-3"
weight="bold"
/>
))}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { LlmModelMetadata } from "./types";
export function groupByCreator(models: LlmModelMetadata[]) {
const map = new Map<string, LlmModelMetadata[]>();
for (const model of models) {
const key = getCreatorDisplayName(model);
const existing = map.get(key) ?? [];
existing.push(model);
map.set(key, existing);
}
return map;
}
export function groupByTitle(models: LlmModelMetadata[]) {
const map = new Map<string, LlmModelMetadata[]>();
for (const model of models) {
const displayName = getModelDisplayName(model);
const existing = map.get(displayName) ?? [];
existing.push(model);
map.set(displayName, existing);
}
return map;
}
export function getCreatorDisplayName(model: LlmModelMetadata): string {
return model.creator_name || model.creator || "";
}
export function getModelDisplayName(model: LlmModelMetadata): string {
return model.title || model.name || "";
}
export function getProviderDisplayName(model: LlmModelMetadata): string {
return model.provider_name || model.provider || "";
}

View File

@@ -0,0 +1,11 @@
export type LlmModelMetadata = {
creator: string;
creator_name: string;
title: string;
provider: string;
provider_name: string;
name: string;
price_tier?: number;
};
export type LlmModelMetadataMap = Record<string, LlmModelMetadata>;

View File

@@ -8,6 +8,7 @@ import {
isMultiSelectSchema,
} from "../utils/schema-utils";
import { TableField } from "./TableField/TableField";
import { LlmModelField } from "./LlmModelField/LlmModelField";
export interface CustomFieldDefinition {
id: string;
@@ -57,6 +58,15 @@ export const CUSTOM_FIELDS: CustomFieldDefinition[] = [
},
component: TableField,
},
{
id: "custom/llm_model_field",
matcher: (schema: any) => {
return (
typeof schema === "object" && schema !== null && "llm_model" in schema
);
},
component: LlmModelField,
},
];
export function findCustomFieldId(schema: any): string | null {

View File

@@ -106,9 +106,14 @@ export function getTimezoneDisplayName(timezone: string): string {
const parts = timezone.split("/");
const city = parts[parts.length - 1].replace(/_/g, " ");
const abbr = getTimezoneAbbreviation(timezone);
return abbr ? `${city} (${abbr})` : city;
if (abbr && abbr !== timezone) {
return `${city} (${abbr})`;
}
// If abbreviation is same as timezone or not found, show timezone with underscores replaced
const timezoneDisplay = timezone.replace(/_/g, " ");
return `${city} (${timezoneDisplay})`;
} catch {
return timezone;
return timezone.replace(/_/g, " ");
}
}