Files

1 line
87 KiB
JSON

{"repo": "ajasmohammed/agos", "files": {"config/providers.toml": {"7cfddf72cc978e879847f3dd19b72d1dca131814": {"t": 1785745329.1361172, "n": 5977, "h": "4426db7559641aa6", "c": "# AgentOS Provider Catalog\n# OpenAI-compatible LLM providers that can be used via the `CustomCore` adapter.\n# Each entry defines a provider's base URL, API key env var, and available models.\n# Use `agentos provider list` to see all available providers and their status.\n\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\", \"gemma2-9b-it\"]\n\n[[provider]]\nname = \"fireworks\"\ndisplay_name = \"Fireworks AI\"\nbase_url = \"https://api.fireworks.ai/inference/v1\"\napi_key_env = \"FIREWORKS_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"accounts/fireworks/models/llama-v3p3-70b-instruct\"\nmodels = [\"accounts/fireworks/models/llama-v3p3-70b-instruct\"]\nvision_models = [\"accounts/fireworks/models/llama-v3p2-90b-vision-instruct\"]\n\n[[provider]]\nname = \"perplexity\"\ndisplay_name = \"Perplexity\"\nbase_url = \"https://api.perplexity.ai\"\napi_key_env = \"PERPLEXITY_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"sonar\"\nmodels = [\"sonar\", \"sonar-pro\", \"sonar-reasoning\"]\nvision_models = [\"sonar\", \"sonar-pro\"]\n\n[[provider]]\nname = \"openrouter\"\ndisplay_name = \"OpenRouter\"\nbase_url = \"https://openrouter.ai/api/v1\"\napi_key_env = \"OPENROUTER_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"auto\"\nmodels = [\"auto\"]\nvision_models = [\"auto\"]\n\n[[provider]]\nname = \"together\"\ndisplay_name = \"Together AI\"\nbase_url = \"https://api.together.xyz/v1\"\napi_key_env = \"TOGETHER_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct-Turbo\", \"mistralai/Mixtral-8x22B-Instruct-v0.1\"]\nvision_models = [\"meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo\", \"meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\n[[provider]]\nname = \"vllm\"\ndisplay_name = \"vLLM\"\nbase_url = \"http://localhost:8000/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"default\"\nmodels = [\"default\"]\n\n[[provider]]\nname = \"deepinfra\"\ndisplay_name = \"DeepInfra\"\nbase_url = \"https://api.deepinfra.com/v1/openai\"\napi_key_env = \"DEEPINFRA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct\"]\nvision_models = [\"meta-llama/Llama-3.2-90B-Vision-Instruct\"]\n\n[[provider]]\nname = \"sambanova\"\ndisplay_name = \"SambaNova\"\nbase_url = \"https://api.sambanova.ai/v1\"\napi_key_env = \"SAMBANOVA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"Meta-Llama-3.3-70B-Instruct\"\nmodels = [\"Meta-Llama-3.3-70B-Instruct\"]\n\n[[provider]]\nname = \"mistral\"\ndisplay_name = \"Mistral AI\"\nbase_url = \"https://api.mistral.ai/v1\"\napi_key_env = \"MISTRAL_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"mistral-large-latest\"\nmodels = [\"mistral-large-latest\", \"mistral-medium-latest\", \"mistral-small-latest\", \"open-mistral-nemo\", \"codestral-latest\", \"pixtral-large-latest\", \"pixtral-12b-2409\"]\nvision_models = [\"pixtral-large-latest\", \"pixtral-12b-2409\"]\n\n[[provider]]\nname = \"xai\"\ndisplay_name = \"xAI (Grok)\"\nbase_url = \"https://api.x.ai/v1\"\napi_key_env = \"XAI_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"grok-3\"\nmodels = [\"grok-3\", \"grok-3-mini\", \"grok-2\", \"grok-2-vision\"]\nvision_models = [\"grok-2-vision\", \"grok-4-vision\"]\n\n[[provider]]\nname = \"cohere\"\ndisplay_name = \"Cohere\"\nbase_url = \"https://api.cohere.com/compatibility/v1\"\napi_key_env = \"COHERE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"command-r-plus\"\nmodels = [\"command-r-plus\", \"command-r\", \"command-a-03-2025\"]\n\n[[provider]]\nname = \"azure\"\ndisplay_name = \"Azure OpenAI\"\nbase_url = \"https://YOUR_RESOURCE.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT\"\napi_key_env = \"AZURE_OPENAI_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"gpt-4o\"\nmodels = [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"]\nvision_models = [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"]\n\n[[provider]]\nname = \"cerebras\"\ndisplay_name = \"Cerebras\"\nbase_url = \"https://api.cerebras.ai/v1\"\napi_key_env = \"CEREBRAS_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama3.1-70b\"\nmodels = [\"llama3.1-70b\", \"llama3.1-8b\", \"llama-3.3-70b\"]\n\n[[provider]]\nname = \"arcee\"\ndisplay_name = \"Arcee AI\"\nbase_url = \"https://conductor.arcee.ai/v1\"\napi_key_env = \"ARCEE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"auto\"\nmodels = [\"auto\", \"arcee-spark\", \"arcee-nova\"]\n\n[[provider]]\nname = \"moonshot\"\ndisplay_name = \"Moonshot AI\"\nbase_url = \"https://api.moonshot.cn/v1\"\napi_key_env = \"MOONSHOT_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"moonshot-v1-128k\"\nmodels = [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]\n\n[[provider]]\nname = \"nvidia\"\ndisplay_name = \"NVIDIA NIM\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta/llama-3.3-70b-instruct\"\nmodels = [\"meta/llama-3.3-70b-instruct\", \"nvidia/llama-3.1-nemotron-70b-instruct\"]\n\n[[provider]]\nname = \"hyperbolic\"\ndisplay_name = \"Hyperbolic\"\nbase_url = \"https://api.hyperbolic.xyz/v1\"\napi_key_env = \"HYPERBOLIC_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct\", \"Qwen/QwQ-32B\"]\n\n[[provider]]\nname = \"anyscale\"\ndisplay_name = \"Anyscale\"\nbase_url = \"https://api.endpoints.anyscale.com/v1\"\napi_key_env = \"ANYSCALE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3-70b-chat-hf\"\nmodels = [\"meta-llama/Llama-3-70b-chat-hf\", \"mistralai/Mixtral-8x7B-Instruct-v0.1\"]\n"}, "0a25512159d416b664d64eefbf29a5183c60289b": {"t": 1785750249.3774848, "n": 5977, "h": "4426db7559641aa6", "c": "# AgentOS Provider Catalog\n# OpenAI-compatible LLM providers that can be used via the `CustomCore` adapter.\n# Each entry defines a provider's base URL, API key env var, and available models.\n# Use `agentos provider list` to see all available providers and their status.\n\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\", \"gemma2-9b-it\"]\n\n[[provider]]\nname = \"fireworks\"\ndisplay_name = \"Fireworks AI\"\nbase_url = \"https://api.fireworks.ai/inference/v1\"\napi_key_env = \"FIREWORKS_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"accounts/fireworks/models/llama-v3p3-70b-instruct\"\nmodels = [\"accounts/fireworks/models/llama-v3p3-70b-instruct\"]\nvision_models = [\"accounts/fireworks/models/llama-v3p2-90b-vision-instruct\"]\n\n[[provider]]\nname = \"perplexity\"\ndisplay_name = \"Perplexity\"\nbase_url = \"https://api.perplexity.ai\"\napi_key_env = \"PERPLEXITY_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"sonar\"\nmodels = [\"sonar\", \"sonar-pro\", \"sonar-reasoning\"]\nvision_models = [\"sonar\", \"sonar-pro\"]\n\n[[provider]]\nname = \"openrouter\"\ndisplay_name = \"OpenRouter\"\nbase_url = \"https://openrouter.ai/api/v1\"\napi_key_env = \"OPENROUTER_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"auto\"\nmodels = [\"auto\"]\nvision_models = [\"auto\"]\n\n[[provider]]\nname = \"together\"\ndisplay_name = \"Together AI\"\nbase_url = \"https://api.together.xyz/v1\"\napi_key_env = \"TOGETHER_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct-Turbo\", \"mistralai/Mixtral-8x22B-Instruct-v0.1\"]\nvision_models = [\"meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo\", \"meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\n[[provider]]\nname = \"vllm\"\ndisplay_name = \"vLLM\"\nbase_url = \"http://localhost:8000/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"default\"\nmodels = [\"default\"]\n\n[[provider]]\nname = \"deepinfra\"\ndisplay_name = \"DeepInfra\"\nbase_url = \"https://api.deepinfra.com/v1/openai\"\napi_key_env = \"DEEPINFRA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct\"]\nvision_models = [\"meta-llama/Llama-3.2-90B-Vision-Instruct\"]\n\n[[provider]]\nname = \"sambanova\"\ndisplay_name = \"SambaNova\"\nbase_url = \"https://api.sambanova.ai/v1\"\napi_key_env = \"SAMBANOVA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"Meta-Llama-3.3-70B-Instruct\"\nmodels = [\"Meta-Llama-3.3-70B-Instruct\"]\n\n[[provider]]\nname = \"mistral\"\ndisplay_name = \"Mistral AI\"\nbase_url = \"https://api.mistral.ai/v1\"\napi_key_env = \"MISTRAL_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"mistral-large-latest\"\nmodels = [\"mistral-large-latest\", \"mistral-medium-latest\", \"mistral-small-latest\", \"open-mistral-nemo\", \"codestral-latest\", \"pixtral-large-latest\", \"pixtral-12b-2409\"]\nvision_models = [\"pixtral-large-latest\", \"pixtral-12b-2409\"]\n\n[[provider]]\nname = \"xai\"\ndisplay_name = \"xAI (Grok)\"\nbase_url = \"https://api.x.ai/v1\"\napi_key_env = \"XAI_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"grok-3\"\nmodels = [\"grok-3\", \"grok-3-mini\", \"grok-2\", \"grok-2-vision\"]\nvision_models = [\"grok-2-vision\", \"grok-4-vision\"]\n\n[[provider]]\nname = \"cohere\"\ndisplay_name = \"Cohere\"\nbase_url = \"https://api.cohere.com/compatibility/v1\"\napi_key_env = \"COHERE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"command-r-plus\"\nmodels = [\"command-r-plus\", \"command-r\", \"command-a-03-2025\"]\n\n[[provider]]\nname = \"azure\"\ndisplay_name = \"Azure OpenAI\"\nbase_url = \"https://YOUR_RESOURCE.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT\"\napi_key_env = \"AZURE_OPENAI_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"gpt-4o\"\nmodels = [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"]\nvision_models = [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"]\n\n[[provider]]\nname = \"cerebras\"\ndisplay_name = \"Cerebras\"\nbase_url = \"https://api.cerebras.ai/v1\"\napi_key_env = \"CEREBRAS_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama3.1-70b\"\nmodels = [\"llama3.1-70b\", \"llama3.1-8b\", \"llama-3.3-70b\"]\n\n[[provider]]\nname = \"arcee\"\ndisplay_name = \"Arcee AI\"\nbase_url = \"https://conductor.arcee.ai/v1\"\napi_key_env = \"ARCEE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"auto\"\nmodels = [\"auto\", \"arcee-spark\", \"arcee-nova\"]\n\n[[provider]]\nname = \"moonshot\"\ndisplay_name = \"Moonshot AI\"\nbase_url = \"https://api.moonshot.cn/v1\"\napi_key_env = \"MOONSHOT_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"moonshot-v1-128k\"\nmodels = [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]\n\n[[provider]]\nname = \"nvidia\"\ndisplay_name = \"NVIDIA NIM\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta/llama-3.3-70b-instruct\"\nmodels = [\"meta/llama-3.3-70b-instruct\", \"nvidia/llama-3.1-nemotron-70b-instruct\"]\n\n[[provider]]\nname = \"hyperbolic\"\ndisplay_name = \"Hyperbolic\"\nbase_url = \"https://api.hyperbolic.xyz/v1\"\napi_key_env = \"HYPERBOLIC_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct\", \"Qwen/QwQ-32B\"]\n\n[[provider]]\nname = \"anyscale\"\ndisplay_name = \"Anyscale\"\nbase_url = \"https://api.endpoints.anyscale.com/v1\"\napi_key_env = \"ANYSCALE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3-70b-chat-hf\"\nmodels = [\"meta-llama/Llama-3-70b-chat-hf\", \"mistralai/Mixtral-8x7B-Instruct-v0.1\"]\n"}, "4ecc8a8f3cec27d4661dd31041dbcc0eba75e600": {"t": 1785750250.4727829, "n": 5445, "h": "2af095e4ef098d76", "c": "# AgentOS Provider Catalog\n# OpenAI-compatible LLM providers that can be used via the `CustomCore` adapter.\n# Each entry defines a provider's base URL, API key env var, and available models.\n# Use `agentos provider list` to see all available providers and their status.\n\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\", \"gemma2-9b-it\"]\n\n[[provider]]\nname = \"fireworks\"\ndisplay_name = \"Fireworks AI\"\nbase_url = \"https://api.fireworks.ai/inference/v1\"\napi_key_env = \"FIREWORKS_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"accounts/fireworks/models/llama-v3p3-70b-instruct\"\nmodels = [\"accounts/fireworks/models/llama-v3p3-70b-instruct\"]\n\n[[provider]]\nname = \"perplexity\"\ndisplay_name = \"Perplexity\"\nbase_url = \"https://api.perplexity.ai\"\napi_key_env = \"PERPLEXITY_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"sonar\"\nmodels = [\"sonar\", \"sonar-pro\", \"sonar-reasoning\"]\n\n[[provider]]\nname = \"openrouter\"\ndisplay_name = \"OpenRouter\"\nbase_url = \"https://openrouter.ai/api/v1\"\napi_key_env = \"OPENROUTER_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"auto\"\nmodels = [\"auto\"]\n\n[[provider]]\nname = \"together\"\ndisplay_name = \"Together AI\"\nbase_url = \"https://api.together.xyz/v1\"\napi_key_env = \"TOGETHER_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct-Turbo\", \"mistralai/Mixtral-8x22B-Instruct-v0.1\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\n[[provider]]\nname = \"vllm\"\ndisplay_name = \"vLLM\"\nbase_url = \"http://localhost:8000/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"default\"\nmodels = [\"default\"]\n\n[[provider]]\nname = \"deepinfra\"\ndisplay_name = \"DeepInfra\"\nbase_url = \"https://api.deepinfra.com/v1/openai\"\napi_key_env = \"DEEPINFRA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct\"]\n\n[[provider]]\nname = \"sambanova\"\ndisplay_name = \"SambaNova\"\nbase_url = \"https://api.sambanova.ai/v1\"\napi_key_env = \"SAMBANOVA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"Meta-Llama-3.3-70B-Instruct\"\nmodels = [\"Meta-Llama-3.3-70B-Instruct\"]\n\n[[provider]]\nname = \"mistral\"\ndisplay_name = \"Mistral AI\"\nbase_url = \"https://api.mistral.ai/v1\"\napi_key_env = \"MISTRAL_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"mistral-large-latest\"\nmodels = [\"mistral-large-latest\", \"mistral-medium-latest\", \"mistral-small-latest\", \"open-mistral-nemo\", \"codestral-latest\"]\n\n[[provider]]\nname = \"xai\"\ndisplay_name = \"xAI (Grok)\"\nbase_url = \"https://api.x.ai/v1\"\napi_key_env = \"XAI_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"grok-3\"\nmodels = [\"grok-3\", \"grok-3-mini\", \"grok-2\", \"grok-2-vision\"]\n\n[[provider]]\nname = \"cohere\"\ndisplay_name = \"Cohere\"\nbase_url = \"https://api.cohere.com/compatibility/v1\"\napi_key_env = \"COHERE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"command-r-plus\"\nmodels = [\"command-r-plus\", \"command-r\", \"command-a-03-2025\"]\n\n[[provider]]\nname = \"azure\"\ndisplay_name = \"Azure OpenAI\"\nbase_url = \"https://YOUR_RESOURCE.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT\"\napi_key_env = \"AZURE_OPENAI_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"gpt-4o\"\nmodels = [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"]\n\n[[provider]]\nname = \"cerebras\"\ndisplay_name = \"Cerebras\"\nbase_url = \"https://api.cerebras.ai/v1\"\napi_key_env = \"CEREBRAS_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama3.1-70b\"\nmodels = [\"llama3.1-70b\", \"llama3.1-8b\", \"llama-3.3-70b\"]\n\n[[provider]]\nname = \"arcee\"\ndisplay_name = \"Arcee AI\"\nbase_url = \"https://conductor.arcee.ai/v1\"\napi_key_env = \"ARCEE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"auto\"\nmodels = [\"auto\", \"arcee-spark\", \"arcee-nova\"]\n\n[[provider]]\nname = \"moonshot\"\ndisplay_name = \"Moonshot AI\"\nbase_url = \"https://api.moonshot.cn/v1\"\napi_key_env = \"MOONSHOT_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"moonshot-v1-128k\"\nmodels = [\"moonshot-v1-8k\", \"moonshot-v1-32k\", \"moonshot-v1-128k\"]\n\n[[provider]]\nname = \"nvidia\"\ndisplay_name = \"NVIDIA NIM\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta/llama-3.3-70b-instruct\"\nmodels = [\"meta/llama-3.3-70b-instruct\", \"nvidia/llama-3.1-nemotron-70b-instruct\"]\n\n[[provider]]\nname = \"hyperbolic\"\ndisplay_name = \"Hyperbolic\"\nbase_url = \"https://api.hyperbolic.xyz/v1\"\napi_key_env = \"HYPERBOLIC_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct\", \"Qwen/QwQ-32B\"]\n\n[[provider]]\nname = \"anyscale\"\ndisplay_name = \"Anyscale\"\nbase_url = \"https://api.endpoints.anyscale.com/v1\"\napi_key_env = \"ANYSCALE_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3-70b-chat-hf\"\nmodels = [\"meta-llama/Llama-3-70b-chat-hf\", \"mistralai/Mixtral-8x7B-Instruct-v0.1\"]\n"}, "27f58d7e0ad2364678debf5ccf83b1d9f96b56d9": {"t": 1785750251.5514688, "n": 2737, "h": "f1a20491f925b641", "c": "# AgentOS Provider Catalog\n# OpenAI-compatible LLM providers that can be used via the `CustomCore` adapter.\n# Each entry defines a provider's base URL, API key env var, and available models.\n# Use `agentos provider list` to see all available providers and their status.\n\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\", \"gemma2-9b-it\"]\n\n[[provider]]\nname = \"fireworks\"\ndisplay_name = \"Fireworks AI\"\nbase_url = \"https://api.fireworks.ai/inference/v1\"\napi_key_env = \"FIREWORKS_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"accounts/fireworks/models/llama-v3p3-70b-instruct\"\nmodels = [\"accounts/fireworks/models/llama-v3p3-70b-instruct\"]\n\n[[provider]]\nname = \"perplexity\"\ndisplay_name = \"Perplexity\"\nbase_url = \"https://api.perplexity.ai\"\napi_key_env = \"PERPLEXITY_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"sonar\"\nmodels = [\"sonar\", \"sonar-pro\", \"sonar-reasoning\"]\n\n[[provider]]\nname = \"openrouter\"\ndisplay_name = \"OpenRouter\"\nbase_url = \"https://openrouter.ai/api/v1\"\napi_key_env = \"OPENROUTER_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"auto\"\nmodels = [\"auto\"]\n\n[[provider]]\nname = \"together\"\ndisplay_name = \"Together AI\"\nbase_url = \"https://api.together.xyz/v1\"\napi_key_env = \"TOGETHER_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct-Turbo\", \"mistralai/Mixtral-8x22B-Instruct-v0.1\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\n[[provider]]\nname = \"vllm\"\ndisplay_name = \"vLLM\"\nbase_url = \"http://localhost:8000/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"default\"\nmodels = [\"default\"]\n\n[[provider]]\nname = \"deepinfra\"\ndisplay_name = \"DeepInfra\"\nbase_url = \"https://api.deepinfra.com/v1/openai\"\napi_key_env = \"DEEPINFRA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"meta-llama/Llama-3.3-70B-Instruct\"\nmodels = [\"meta-llama/Llama-3.3-70B-Instruct\"]\n\n[[provider]]\nname = \"sambanova\"\ndisplay_name = \"SambaNova\"\nbase_url = \"https://api.sambanova.ai/v1\"\napi_key_env = \"SAMBANOVA_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"Meta-Llama-3.3-70B-Instruct\"\nmodels = [\"Meta-Llama-3.3-70B-Instruct\"]\n"}}, "crates/agentos-llm/src/catalog.rs": {"7cfddf72cc978e879847f3dd19b72d1dca131814": {"t": 1785745875.1326838, "n": 20835, "h": "ba98dbfabc161dce", "c": "use serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\nuse std::path::Path;\n\n/// A single provider entry from the `providers.toml` catalog.\n///\n/// All \"override\" fields are optional; absent values fall back to OpenAI-compatible\n/// defaults so existing catalog files continue to parse without modification.\n#[derive(Debug, Clone, Deserialize, Serialize, Default)]\npub struct CatalogEntry {\n pub name: String,\n pub display_name: String,\n pub base_url: String,\n pub api_key_env: String,\n pub compatible_with: String,\n pub default_model: String,\n #[serde(default)]\n pub models: Vec<String>,\n /// Model IDs that accept OpenAI-style `image_url` parts (CustomCore / openai-compat).\n #[serde(default)]\n pub vision_models: Vec<String>,\n\n // ---- Capability overrides (apply to all models for this provider) ----\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub context_window: Option<u64>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub max_output_tokens: Option<u64>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_images: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_tool_calling: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_streaming: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_prompt_caching: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_json_mode: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_thinking: Option<bool>,\n\n // ---- Auth / endpoint overrides ----\n /// HTTP header used for auth. Defaults to `Authorization`. Use `api-key`\n /// for Azure-style providers.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub auth_header: Option<String>,\n /// Prefix prepended to the API key in the auth header. Defaults to\n /// `\"Bearer \"`. Use `\"\"` for providers that pass the bare key.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub auth_prefix: Option<String>,\n /// Chat completions path appended to `base_url`. Default `/chat/completions`.\n /// May include query string (e.g. `?api-version=2024-08-01`).\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub chat_path: Option<String>,\n /// Models list path appended to `base_url`. Default `/models`.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub models_path: Option<String>,\n\n /// Static extra headers attached to every request (e.g. tenancy IDs).\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub extra_headers: Option<HashMap<String, String>>,\n\n /// Permit a private/loopback/link-local `base_url`. Required for legitimate\n /// local providers (lmstudio, ollama, vllm). Catalog validation rejects\n /// private addresses unless this is `Some(true)`.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub allow_private_hosts: Option<bool>,\n}\n\nimpl CatalogEntry {\n pub fn auth_header_name(&self) -> &str {\n self.auth_header.as_deref().unwrap_or(\"Authorization\")\n }\n pub fn auth_header_prefix(&self) -> &str {\n self.auth_prefix.as_deref().unwrap_or(\"Bearer \")\n }\n pub fn chat_path_or_default(&self) -> &str {\n self.chat_path.as_deref().unwrap_or(\"/chat/completions\")\n }\n pub fn models_path_or_default(&self) -> &str {\n self.models_path.as_deref().unwrap_or(\"/models\")\n }\n}\n\n/// Parsed provider catalog loaded from a TOML file.\n///\n/// The catalog maps provider names to their configuration entries, allowing\n/// the kernel to auto-configure `CustomCore` instances for OpenAI-compatible\n/// providers without requiring manual `--base-url` flags.\n#[derive(Debug, Clone)]\npub struct ProviderCatalog {\n providers: HashMap<String, CatalogEntry>,\n}\n\n/// Top-level TOML structure: `[[provider]]` array.\n#[derive(Debug, Deserialize)]\nstruct CatalogFile {\n #[serde(default)]\n provider: Vec<CatalogEntry>,\n}\n\nimpl ProviderCatalog {\n /// Parse a catalog from a TOML string. Keys are normalised to lowercase so\n /// `lookup`, `set_models`, `remove`, etc. all hit a single canonical entry\n /// regardless of how the user cased the `name = \"…\"` field.\n pub fn parse(toml_str: &str) -> Result<Self, toml::de::Error> {\n let file: CatalogFile = toml::from_str(toml_str)?;\n let mut providers = HashMap::new();\n for entry in file.provider {\n providers.insert(entry.name.to_lowercase(), entry);\n }\n Ok(Self { providers })\n }\n\n /// Load a catalog from a file path. Returns an empty catalog if the file\n /// does not exist (this is not an error — the catalog is optional).\n pub fn from_file(path: &Path) -> Result<Self, String> {\n if !path.exists() {\n return Ok(Self::empty());\n }\n let content = std::fs::read_to_string(path).map_err(|e| {\n format!(\n \"Failed to read provider catalog at {}: {}\",\n path.display(),\n e\n )\n })?;\n Self::parse(&content).map_err(|e| format!(\"Failed to parse provider catalog: {}\", e))\n }\n\n /// Create an empty catalog with no providers.\n pub fn empty() -> Self {\n Self {\n providers: HashMap::new(),\n }\n }\n\n /// Look up a provider by name (case-insensitive).\n pub fn lookup(&self, name: &str) -> Option<&CatalogEntry> {\n self.providers\n .get(&name.to_lowercase())\n .or_else(|| self.providers.get(name))\n }\n\n /// Return all catalog entries sorted by name.\n pub fn list(&self) -> Vec<&CatalogEntry> {\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n entries\n }\n\n /// Check if the catalog has no entries.\n pub fn is_empty(&self) -> bool {\n self.providers.is_empty()\n }\n\n /// Number of entries in the catalog.\n pub fn len(&self) -> usize {\n self.providers.len()\n }\n\n /// Return a cloned copy of this catalog (used for snapshot-before-write).\n pub fn clone_inner(&self) -> Self {\n Self {\n providers: self.providers.clone(),\n }\n }\n\n /// Override the base URL for an existing provider. Returns `true` if the\n /// provider was found and updated, `false` if the name is unknown.\n pub fn set_base_url(&mut self, name: &str, url: String) -> bool {\n let key = name.to_lowercase();\n if let Some(entry) = self.providers.get_mut(&key) {\n entry.base_url = url;\n true\n } else if let Some(entry) = self.providers.get_mut(name) {\n entry.base_url = url;\n true\n } else {\n false\n }\n }\n\n /// Insert or replace a provider entry. Returns `true` when an existing\n /// entry was replaced, `false` when this is a fresh insert.\n pub fn upsert(&mut self, entry: CatalogEntry) -> bool {\n let key = entry.name.to_lowercase();\n let existed = self.providers.contains_key(&key) || self.providers.contains_key(&entry.name);\n // Drop any case-variant duplicates first.\n self.providers.remove(&entry.name);\n self.providers.insert(key, entry);\n existed\n }\n\n /// Remove a provider by name. Returns the removed entry if present.\n pub fn remove(&mut self, name: &str) -> Option<CatalogEntry> {\n let key = name.to_lowercase();\n self.providers\n .remove(&key)\n .or_else(|| self.providers.remove(name))\n }\n\n /// Replace the `models` array of an existing entry. Used by the\n /// `/models` auto-probe. Returns `true` on success.\n pub fn set_models(&mut self, name: &str, models: Vec<String>) -> bool {\n let key = name.to_lowercase();\n if let Some(entry) = self.providers.get_mut(&key) {\n entry.models = models;\n true\n } else if let Some(entry) = self.providers.get_mut(name) {\n entry.models = models;\n true\n } else {\n false\n }\n }\n\n /// Serialize the catalog back to TOML and write it to `path`.\n pub fn save_to_file(&self, path: &std::path::Path) -> Result<(), String> {\n let mut lines = String::new();\n // Sort entries for stable output\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n for entry in entries {\n lines.push_str(\"[[provider]]\\n\");\n lines.push_str(&format!(\"name = {:?}\\n\", entry.name));\n lines.push_str(&format!(\"display_name = {:?}\\n\", entry.display_name));\n lines.push_str(&format!(\"base_url = {:?}\\n\", entry.base_url));\n lines.push_str(&format!(\"api_key_env = {:?}\\n\", entry.api_key_env));\n lines.push_str(&format!(\"compatible_with = {:?}\\n\", entry.compatible_with));\n lines.push_str(&format!(\"default_model = {:?}\\n\", entry.default_model));\n if !entry.models.is_empty() {\n let models_str = entry\n .models\n .iter()\n .map(|m| format!(\"{:?}\", m))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"models = [{}]\\n\", models_str));\n }\n if !entry.vision_models.is_empty() {\n let vm_str = entry\n .vision_models\n .iter()\n .map(|m| format!(\"{:?}\", m))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"vision_models = [{}]\\n\", vm_str));\n }\n // Optional capability + auth + path overrides.\n if let Some(v) = entry.context_window {\n lines.push_str(&format!(\"context_window = {}\\n\", v));\n }\n if let Some(v) = entry.max_output_tokens {\n lines.push_str(&format!(\"max_output_tokens = {}\\n\", v));\n }\n if let Some(v) = entry.supports_images {\n lines.push_str(&format!(\"supports_images = {}\\n\", v));\n }\n if let Some(v) = entry.supports_tool_calling {\n lines.push_str(&format!(\"supports_tool_calling = {}\\n\", v));\n }\n if let Some(v) = entry.supports_streaming {\n lines.push_str(&format!(\"supports_streaming = {}\\n\", v));\n }\n if let Some(v) = entry.supports_prompt_caching {\n lines.push_str(&format!(\"supports_prompt_caching = {}\\n\", v));\n }\n if let Some(v) = entry.supports_json_mode {\n lines.push_str(&format!(\"supports_json_mode = {}\\n\", v));\n }\n if let Some(v) = entry.supports_thinking {\n lines.push_str(&format!(\"supports_thinking = {}\\n\", v));\n }\n if let Some(v) = &entry.auth_header {\n lines.push_str(&format!(\"auth_header = {:?}\\n\", v));\n }\n if let Some(v) = &entry.auth_prefix {\n lines.push_str(&format!(\"auth_prefix = {:?}\\n\", v));\n }\n if let Some(v) = &entry.chat_path {\n lines.push_str(&format!(\"chat_path = {:?}\\n\", v));\n }\n if let Some(v) = &entry.models_path {\n lines.push_str(&format!(\"models_path = {:?}\\n\", v));\n }\n if let Some(map) = &entry.extra_headers {\n if !map.is_empty() {\n let mut keys: Vec<&String> = map.keys().collect();\n keys.sort();\n let body = keys\n .iter()\n .map(|k| format!(\"{:?} = {:?}\", k, map[*k]))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"extra_headers = {{ {} }}\\n\", body));\n }\n }\n if let Some(v) = entry.allow_private_hosts {\n lines.push_str(&format!(\"allow_private_hosts = {}\\n\", v));\n }\n lines.push('\\n');\n }\n // Atomic write: stage to a sibling `.tmp` file then rename. A crash\n // mid-write leaves the original `providers.toml` intact rather than\n // truncated.\n let tmp_path = path.with_extension(\"toml.tmp\");\n std::fs::write(&tmp_path, lines)\n .map_err(|e| format!(\"Failed to write provider catalog (tmp): {}\", e))?;\n std::fs::rename(&tmp_path, path).map_err(|e| {\n // Best-effort cleanup of the staged file.\n let _ = std::fs::remove_file(&tmp_path);\n format!(\"Failed to rename provider catalog into place: {}\", e)\n })\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n const TEST_TOML: &str = r#\"\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\"#;\n\n #[test]\n fn test_parse_catalog() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert_eq!(catalog.len(), 3);\n assert!(!catalog.is_empty());\n }\n\n #[test]\n fn test_lookup_existing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entry = catalog.lookup(\"deepseek\").expect(\"should find deepseek\");\n assert_eq!(entry.display_name, \"DeepSeek\");\n assert_eq!(entry.base_url, \"https://api.deepseek.com\");\n assert_eq!(entry.default_model, \"deepseek-chat\");\n assert_eq!(entry.models.len(), 2);\n }\n\n #[test]\n fn test_lookup_missing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert!(catalog.lookup(\"nonexistent\").is_none());\n }\n\n #[test]\n fn test_list_sorted() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entries = catalog.list();\n assert_eq!(entries.len(), 3);\n assert_eq!(entries[0].name, \"deepseek\");\n assert_eq!(entries[1].name, \"groq\");\n assert_eq!(entries[2].name, \"lmstudio\");\n }\n\n #[test]\n fn test_empty_catalog() {\n let catalog = ProviderCatalog::empty();\n assert!(catalog.is_empty());\n assert_eq!(catalog.len(), 0);\n assert!(catalog.lookup(\"anything\").is_none());\n }\n\n #[test]\n fn test_from_file_nonexistent() {\n let catalog = ProviderCatalog::from_file(Path::new(\"/nonexistent/providers.toml\")).unwrap();\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_empty_provider_list() {\n let catalog = ProviderCatalog::parse(\"\").expect(\"empty string should parse\");\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_no_api_key_env() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let lmstudio = catalog.lookup(\"lmstudio\").expect(\"should find lmstudio\");\n assert_eq!(lmstudio.api_key_env, \"\");\n }\n\n #[test]\n fn test_default_overrides_used_when_unset() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entry = catalog.lookup(\"deepseek\").unwrap();\n assert_eq!(entry.auth_header_name(), \"Authorization\");\n assert_eq!(entry.auth_header_prefix(), \"Bearer \");\n assert_eq!(entry.chat_path_or_default(), \"/chat/completions\");\n assert_eq!(entry.models_path_or_default(), \"/models\");\n }\n\n #[test]\n fn test_overrides_parsed() {\n let toml = r#\"\n[[provider]]\nname = \"azure\"\ndisplay_name = \"Azure\"\nbase_url = \"https://x.openai.azure.com/openai\"\napi_key_env = \"AZURE_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"gpt-4o\"\nauth_header = \"api-key\"\nauth_prefix = \"\"\nchat_path = \"/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview\"\ncontext_window = 128000\nsupports_images = true\n[provider.extra_headers]\n\"x-ms-region\" = \"eastus\"\n\"#;\n let catalog = ProviderCatalog::parse(toml).expect(\"parse\");\n let e = catalog.lookup(\"azure\").unwrap();\n assert_eq!(e.auth_header_name(), \"api-key\");\n assert_eq!(e.auth_header_prefix(), \"\");\n assert!(e.chat_path_or_default().contains(\"api-version\"));\n assert_eq!(e.context_window, Some(128000));\n assert_eq!(e.supports_images, Some(true));\n let hdrs = e.extra_headers.as_ref().unwrap();\n assert_eq!(hdrs.get(\"x-ms-region\").map(String::as_str), Some(\"eastus\"));\n }\n\n #[test]\n fn test_upsert_and_remove() {\n let mut catalog = ProviderCatalog::empty();\n let entry = CatalogEntry {\n name: \"foo\".into(),\n display_name: \"Foo\".into(),\n base_url: \"https://foo\".into(),\n api_key_env: \"FOO_KEY\".into(),\n compatible_with: \"openai\".into(),\n default_model: \"f1\".into(),\n ..Default::default()\n };\n assert!(!catalog.upsert(entry.clone()));\n assert_eq!(catalog.len(), 1);\n // Replace.\n let mut e2 = entry.clone();\n e2.display_name = \"Foo2\".into();\n assert!(catalog.upsert(e2));\n assert_eq!(catalog.lookup(\"foo\").unwrap().display_name, \"Foo2\");\n // Remove.\n let removed = catalog.remove(\"FOO\");\n assert!(removed.is_some());\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_parse_lowercases_mixed_case_names() {\n let toml = r#\"\n[[provider]]\nname = \"DeepSeek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\n\"#;\n let mut catalog = ProviderCatalog::parse(toml).unwrap();\n // Lookups under any case should hit the entry.\n assert!(catalog.lookup(\"deepseek\").is_some());\n assert!(catalog.lookup(\"DeepSeek\").is_some());\n assert!(catalog.lookup(\"DEEPSEEK\").is_some());\n // Mutations resolve the canonical key.\n assert!(catalog.set_models(\"DeepSeek\", vec![\"m1\".into()]));\n assert_eq!(catalog.lookup(\"deepseek\").unwrap().models, vec![\"m1\"]);\n assert!(catalog.set_base_url(\"DEEPSEEK\", \"https://x\".into()));\n assert!(catalog.remove(\"DeepSeek\").is_some());\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_set_models() {\n let mut catalog = ProviderCatalog::parse(TEST_TOML).unwrap();\n assert!(catalog.set_models(\"deepseek\", vec![\"new-model\".into()]));\n assert_eq!(\n catalog.lookup(\"deepseek\").unwrap().models,\n vec![\"new-model\"]\n );\n assert!(!catalog.set_models(\"nonexistent\", vec![]));\n }\n\n #[test]\n fn test_save_round_trip_with_overrides() {\n let mut catalog = ProviderCatalog::empty();\n let mut hdrs = HashMap::new();\n hdrs.insert(\"x-tenant\".into(), \"abc\".into());\n catalog.upsert(CatalogEntry {\n name: \"azure\".into(),\n display_name: \"Azure\".into(),\n base_url: \"https://x\".into(),\n api_key_env: \"AZ\".into(),\n compatible_with: \"openai\".into(),\n default_model: \"gpt-4o\".into(),\n context_window: Some(128_000),\n supports_images: Some(true),\n auth_header: Some(\"api-key\".into()),\n auth_prefix: Some(String::new()),\n chat_path: Some(\"/foo\".into()),\n extra_headers: Some(hdrs),\n ..Default::default()\n });\n let dir = tempfile::tempdir().unwrap();\n let path = dir.path().join(\"providers.toml\");\n catalog.save_to_file(&path).unwrap();\n let loaded = ProviderCatalog::from_file(&path).unwrap();\n let e = loaded.lookup(\"azure\").unwrap();\n assert_eq!(e.context_window, Some(128_000));\n assert_eq!(e.supports_images, Some(true));\n assert_eq!(e.auth_header_name(), \"api-key\");\n assert_eq!(e.auth_header_prefix(), \"\");\n assert_eq!(e.chat_path.as_deref(), Some(\"/foo\"));\n assert_eq!(\n e.extra_headers\n .as_ref()\n .and_then(|m| m.get(\"x-tenant\"))\n .map(String::as_str),\n Some(\"abc\")\n );\n }\n}\n"}, "93930a2f2fa94850ea85937b3fae9706e1facec0": {"t": 1785750250.3460014, "n": 20835, "h": "ba98dbfabc161dce", "c": "use serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\nuse std::path::Path;\n\n/// A single provider entry from the `providers.toml` catalog.\n///\n/// All \"override\" fields are optional; absent values fall back to OpenAI-compatible\n/// defaults so existing catalog files continue to parse without modification.\n#[derive(Debug, Clone, Deserialize, Serialize, Default)]\npub struct CatalogEntry {\n pub name: String,\n pub display_name: String,\n pub base_url: String,\n pub api_key_env: String,\n pub compatible_with: String,\n pub default_model: String,\n #[serde(default)]\n pub models: Vec<String>,\n /// Model IDs that accept OpenAI-style `image_url` parts (CustomCore / openai-compat).\n #[serde(default)]\n pub vision_models: Vec<String>,\n\n // ---- Capability overrides (apply to all models for this provider) ----\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub context_window: Option<u64>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub max_output_tokens: Option<u64>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_images: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_tool_calling: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_streaming: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_prompt_caching: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_json_mode: Option<bool>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub supports_thinking: Option<bool>,\n\n // ---- Auth / endpoint overrides ----\n /// HTTP header used for auth. Defaults to `Authorization`. Use `api-key`\n /// for Azure-style providers.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub auth_header: Option<String>,\n /// Prefix prepended to the API key in the auth header. Defaults to\n /// `\"Bearer \"`. Use `\"\"` for providers that pass the bare key.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub auth_prefix: Option<String>,\n /// Chat completions path appended to `base_url`. Default `/chat/completions`.\n /// May include query string (e.g. `?api-version=2024-08-01`).\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub chat_path: Option<String>,\n /// Models list path appended to `base_url`. Default `/models`.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub models_path: Option<String>,\n\n /// Static extra headers attached to every request (e.g. tenancy IDs).\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub extra_headers: Option<HashMap<String, String>>,\n\n /// Permit a private/loopback/link-local `base_url`. Required for legitimate\n /// local providers (lmstudio, ollama, vllm). Catalog validation rejects\n /// private addresses unless this is `Some(true)`.\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub allow_private_hosts: Option<bool>,\n}\n\nimpl CatalogEntry {\n pub fn auth_header_name(&self) -> &str {\n self.auth_header.as_deref().unwrap_or(\"Authorization\")\n }\n pub fn auth_header_prefix(&self) -> &str {\n self.auth_prefix.as_deref().unwrap_or(\"Bearer \")\n }\n pub fn chat_path_or_default(&self) -> &str {\n self.chat_path.as_deref().unwrap_or(\"/chat/completions\")\n }\n pub fn models_path_or_default(&self) -> &str {\n self.models_path.as_deref().unwrap_or(\"/models\")\n }\n}\n\n/// Parsed provider catalog loaded from a TOML file.\n///\n/// The catalog maps provider names to their configuration entries, allowing\n/// the kernel to auto-configure `CustomCore` instances for OpenAI-compatible\n/// providers without requiring manual `--base-url` flags.\n#[derive(Debug, Clone)]\npub struct ProviderCatalog {\n providers: HashMap<String, CatalogEntry>,\n}\n\n/// Top-level TOML structure: `[[provider]]` array.\n#[derive(Debug, Deserialize)]\nstruct CatalogFile {\n #[serde(default)]\n provider: Vec<CatalogEntry>,\n}\n\nimpl ProviderCatalog {\n /// Parse a catalog from a TOML string. Keys are normalised to lowercase so\n /// `lookup`, `set_models`, `remove`, etc. all hit a single canonical entry\n /// regardless of how the user cased the `name = \"…\"` field.\n pub fn parse(toml_str: &str) -> Result<Self, toml::de::Error> {\n let file: CatalogFile = toml::from_str(toml_str)?;\n let mut providers = HashMap::new();\n for entry in file.provider {\n providers.insert(entry.name.to_lowercase(), entry);\n }\n Ok(Self { providers })\n }\n\n /// Load a catalog from a file path. Returns an empty catalog if the file\n /// does not exist (this is not an error — the catalog is optional).\n pub fn from_file(path: &Path) -> Result<Self, String> {\n if !path.exists() {\n return Ok(Self::empty());\n }\n let content = std::fs::read_to_string(path).map_err(|e| {\n format!(\n \"Failed to read provider catalog at {}: {}\",\n path.display(),\n e\n )\n })?;\n Self::parse(&content).map_err(|e| format!(\"Failed to parse provider catalog: {}\", e))\n }\n\n /// Create an empty catalog with no providers.\n pub fn empty() -> Self {\n Self {\n providers: HashMap::new(),\n }\n }\n\n /// Look up a provider by name (case-insensitive).\n pub fn lookup(&self, name: &str) -> Option<&CatalogEntry> {\n self.providers\n .get(&name.to_lowercase())\n .or_else(|| self.providers.get(name))\n }\n\n /// Return all catalog entries sorted by name.\n pub fn list(&self) -> Vec<&CatalogEntry> {\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n entries\n }\n\n /// Check if the catalog has no entries.\n pub fn is_empty(&self) -> bool {\n self.providers.is_empty()\n }\n\n /// Number of entries in the catalog.\n pub fn len(&self) -> usize {\n self.providers.len()\n }\n\n /// Return a cloned copy of this catalog (used for snapshot-before-write).\n pub fn clone_inner(&self) -> Self {\n Self {\n providers: self.providers.clone(),\n }\n }\n\n /// Override the base URL for an existing provider. Returns `true` if the\n /// provider was found and updated, `false` if the name is unknown.\n pub fn set_base_url(&mut self, name: &str, url: String) -> bool {\n let key = name.to_lowercase();\n if let Some(entry) = self.providers.get_mut(&key) {\n entry.base_url = url;\n true\n } else if let Some(entry) = self.providers.get_mut(name) {\n entry.base_url = url;\n true\n } else {\n false\n }\n }\n\n /// Insert or replace a provider entry. Returns `true` when an existing\n /// entry was replaced, `false` when this is a fresh insert.\n pub fn upsert(&mut self, entry: CatalogEntry) -> bool {\n let key = entry.name.to_lowercase();\n let existed = self.providers.contains_key(&key) || self.providers.contains_key(&entry.name);\n // Drop any case-variant duplicates first.\n self.providers.remove(&entry.name);\n self.providers.insert(key, entry);\n existed\n }\n\n /// Remove a provider by name. Returns the removed entry if present.\n pub fn remove(&mut self, name: &str) -> Option<CatalogEntry> {\n let key = name.to_lowercase();\n self.providers\n .remove(&key)\n .or_else(|| self.providers.remove(name))\n }\n\n /// Replace the `models` array of an existing entry. Used by the\n /// `/models` auto-probe. Returns `true` on success.\n pub fn set_models(&mut self, name: &str, models: Vec<String>) -> bool {\n let key = name.to_lowercase();\n if let Some(entry) = self.providers.get_mut(&key) {\n entry.models = models;\n true\n } else if let Some(entry) = self.providers.get_mut(name) {\n entry.models = models;\n true\n } else {\n false\n }\n }\n\n /// Serialize the catalog back to TOML and write it to `path`.\n pub fn save_to_file(&self, path: &std::path::Path) -> Result<(), String> {\n let mut lines = String::new();\n // Sort entries for stable output\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n for entry in entries {\n lines.push_str(\"[[provider]]\\n\");\n lines.push_str(&format!(\"name = {:?}\\n\", entry.name));\n lines.push_str(&format!(\"display_name = {:?}\\n\", entry.display_name));\n lines.push_str(&format!(\"base_url = {:?}\\n\", entry.base_url));\n lines.push_str(&format!(\"api_key_env = {:?}\\n\", entry.api_key_env));\n lines.push_str(&format!(\"compatible_with = {:?}\\n\", entry.compatible_with));\n lines.push_str(&format!(\"default_model = {:?}\\n\", entry.default_model));\n if !entry.models.is_empty() {\n let models_str = entry\n .models\n .iter()\n .map(|m| format!(\"{:?}\", m))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"models = [{}]\\n\", models_str));\n }\n if !entry.vision_models.is_empty() {\n let vm_str = entry\n .vision_models\n .iter()\n .map(|m| format!(\"{:?}\", m))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"vision_models = [{}]\\n\", vm_str));\n }\n // Optional capability + auth + path overrides.\n if let Some(v) = entry.context_window {\n lines.push_str(&format!(\"context_window = {}\\n\", v));\n }\n if let Some(v) = entry.max_output_tokens {\n lines.push_str(&format!(\"max_output_tokens = {}\\n\", v));\n }\n if let Some(v) = entry.supports_images {\n lines.push_str(&format!(\"supports_images = {}\\n\", v));\n }\n if let Some(v) = entry.supports_tool_calling {\n lines.push_str(&format!(\"supports_tool_calling = {}\\n\", v));\n }\n if let Some(v) = entry.supports_streaming {\n lines.push_str(&format!(\"supports_streaming = {}\\n\", v));\n }\n if let Some(v) = entry.supports_prompt_caching {\n lines.push_str(&format!(\"supports_prompt_caching = {}\\n\", v));\n }\n if let Some(v) = entry.supports_json_mode {\n lines.push_str(&format!(\"supports_json_mode = {}\\n\", v));\n }\n if let Some(v) = entry.supports_thinking {\n lines.push_str(&format!(\"supports_thinking = {}\\n\", v));\n }\n if let Some(v) = &entry.auth_header {\n lines.push_str(&format!(\"auth_header = {:?}\\n\", v));\n }\n if let Some(v) = &entry.auth_prefix {\n lines.push_str(&format!(\"auth_prefix = {:?}\\n\", v));\n }\n if let Some(v) = &entry.chat_path {\n lines.push_str(&format!(\"chat_path = {:?}\\n\", v));\n }\n if let Some(v) = &entry.models_path {\n lines.push_str(&format!(\"models_path = {:?}\\n\", v));\n }\n if let Some(map) = &entry.extra_headers {\n if !map.is_empty() {\n let mut keys: Vec<&String> = map.keys().collect();\n keys.sort();\n let body = keys\n .iter()\n .map(|k| format!(\"{:?} = {:?}\", k, map[*k]))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"extra_headers = {{ {} }}\\n\", body));\n }\n }\n if let Some(v) = entry.allow_private_hosts {\n lines.push_str(&format!(\"allow_private_hosts = {}\\n\", v));\n }\n lines.push('\\n');\n }\n // Atomic write: stage to a sibling `.tmp` file then rename. A crash\n // mid-write leaves the original `providers.toml` intact rather than\n // truncated.\n let tmp_path = path.with_extension(\"toml.tmp\");\n std::fs::write(&tmp_path, lines)\n .map_err(|e| format!(\"Failed to write provider catalog (tmp): {}\", e))?;\n std::fs::rename(&tmp_path, path).map_err(|e| {\n // Best-effort cleanup of the staged file.\n let _ = std::fs::remove_file(&tmp_path);\n format!(\"Failed to rename provider catalog into place: {}\", e)\n })\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n const TEST_TOML: &str = r#\"\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\"#;\n\n #[test]\n fn test_parse_catalog() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert_eq!(catalog.len(), 3);\n assert!(!catalog.is_empty());\n }\n\n #[test]\n fn test_lookup_existing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entry = catalog.lookup(\"deepseek\").expect(\"should find deepseek\");\n assert_eq!(entry.display_name, \"DeepSeek\");\n assert_eq!(entry.base_url, \"https://api.deepseek.com\");\n assert_eq!(entry.default_model, \"deepseek-chat\");\n assert_eq!(entry.models.len(), 2);\n }\n\n #[test]\n fn test_lookup_missing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert!(catalog.lookup(\"nonexistent\").is_none());\n }\n\n #[test]\n fn test_list_sorted() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entries = catalog.list();\n assert_eq!(entries.len(), 3);\n assert_eq!(entries[0].name, \"deepseek\");\n assert_eq!(entries[1].name, \"groq\");\n assert_eq!(entries[2].name, \"lmstudio\");\n }\n\n #[test]\n fn test_empty_catalog() {\n let catalog = ProviderCatalog::empty();\n assert!(catalog.is_empty());\n assert_eq!(catalog.len(), 0);\n assert!(catalog.lookup(\"anything\").is_none());\n }\n\n #[test]\n fn test_from_file_nonexistent() {\n let catalog = ProviderCatalog::from_file(Path::new(\"/nonexistent/providers.toml\")).unwrap();\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_empty_provider_list() {\n let catalog = ProviderCatalog::parse(\"\").expect(\"empty string should parse\");\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_no_api_key_env() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let lmstudio = catalog.lookup(\"lmstudio\").expect(\"should find lmstudio\");\n assert_eq!(lmstudio.api_key_env, \"\");\n }\n\n #[test]\n fn test_default_overrides_used_when_unset() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entry = catalog.lookup(\"deepseek\").unwrap();\n assert_eq!(entry.auth_header_name(), \"Authorization\");\n assert_eq!(entry.auth_header_prefix(), \"Bearer \");\n assert_eq!(entry.chat_path_or_default(), \"/chat/completions\");\n assert_eq!(entry.models_path_or_default(), \"/models\");\n }\n\n #[test]\n fn test_overrides_parsed() {\n let toml = r#\"\n[[provider]]\nname = \"azure\"\ndisplay_name = \"Azure\"\nbase_url = \"https://x.openai.azure.com/openai\"\napi_key_env = \"AZURE_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"gpt-4o\"\nauth_header = \"api-key\"\nauth_prefix = \"\"\nchat_path = \"/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview\"\ncontext_window = 128000\nsupports_images = true\n[provider.extra_headers]\n\"x-ms-region\" = \"eastus\"\n\"#;\n let catalog = ProviderCatalog::parse(toml).expect(\"parse\");\n let e = catalog.lookup(\"azure\").unwrap();\n assert_eq!(e.auth_header_name(), \"api-key\");\n assert_eq!(e.auth_header_prefix(), \"\");\n assert!(e.chat_path_or_default().contains(\"api-version\"));\n assert_eq!(e.context_window, Some(128000));\n assert_eq!(e.supports_images, Some(true));\n let hdrs = e.extra_headers.as_ref().unwrap();\n assert_eq!(hdrs.get(\"x-ms-region\").map(String::as_str), Some(\"eastus\"));\n }\n\n #[test]\n fn test_upsert_and_remove() {\n let mut catalog = ProviderCatalog::empty();\n let entry = CatalogEntry {\n name: \"foo\".into(),\n display_name: \"Foo\".into(),\n base_url: \"https://foo\".into(),\n api_key_env: \"FOO_KEY\".into(),\n compatible_with: \"openai\".into(),\n default_model: \"f1\".into(),\n ..Default::default()\n };\n assert!(!catalog.upsert(entry.clone()));\n assert_eq!(catalog.len(), 1);\n // Replace.\n let mut e2 = entry.clone();\n e2.display_name = \"Foo2\".into();\n assert!(catalog.upsert(e2));\n assert_eq!(catalog.lookup(\"foo\").unwrap().display_name, \"Foo2\");\n // Remove.\n let removed = catalog.remove(\"FOO\");\n assert!(removed.is_some());\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_parse_lowercases_mixed_case_names() {\n let toml = r#\"\n[[provider]]\nname = \"DeepSeek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\n\"#;\n let mut catalog = ProviderCatalog::parse(toml).unwrap();\n // Lookups under any case should hit the entry.\n assert!(catalog.lookup(\"deepseek\").is_some());\n assert!(catalog.lookup(\"DeepSeek\").is_some());\n assert!(catalog.lookup(\"DEEPSEEK\").is_some());\n // Mutations resolve the canonical key.\n assert!(catalog.set_models(\"DeepSeek\", vec![\"m1\".into()]));\n assert_eq!(catalog.lookup(\"deepseek\").unwrap().models, vec![\"m1\"]);\n assert!(catalog.set_base_url(\"DEEPSEEK\", \"https://x\".into()));\n assert!(catalog.remove(\"DeepSeek\").is_some());\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_set_models() {\n let mut catalog = ProviderCatalog::parse(TEST_TOML).unwrap();\n assert!(catalog.set_models(\"deepseek\", vec![\"new-model\".into()]));\n assert_eq!(\n catalog.lookup(\"deepseek\").unwrap().models,\n vec![\"new-model\"]\n );\n assert!(!catalog.set_models(\"nonexistent\", vec![]));\n }\n\n #[test]\n fn test_save_round_trip_with_overrides() {\n let mut catalog = ProviderCatalog::empty();\n let mut hdrs = HashMap::new();\n hdrs.insert(\"x-tenant\".into(), \"abc\".into());\n catalog.upsert(CatalogEntry {\n name: \"azure\".into(),\n display_name: \"Azure\".into(),\n base_url: \"https://x\".into(),\n api_key_env: \"AZ\".into(),\n compatible_with: \"openai\".into(),\n default_model: \"gpt-4o\".into(),\n context_window: Some(128_000),\n supports_images: Some(true),\n auth_header: Some(\"api-key\".into()),\n auth_prefix: Some(String::new()),\n chat_path: Some(\"/foo\".into()),\n extra_headers: Some(hdrs),\n ..Default::default()\n });\n let dir = tempfile::tempdir().unwrap();\n let path = dir.path().join(\"providers.toml\");\n catalog.save_to_file(&path).unwrap();\n let loaded = ProviderCatalog::from_file(&path).unwrap();\n let e = loaded.lookup(\"azure\").unwrap();\n assert_eq!(e.context_window, Some(128_000));\n assert_eq!(e.supports_images, Some(true));\n assert_eq!(e.auth_header_name(), \"api-key\");\n assert_eq!(e.auth_header_prefix(), \"\");\n assert_eq!(e.chat_path.as_deref(), Some(\"/foo\"));\n assert_eq!(\n e.extra_headers\n .as_ref()\n .and_then(|m| m.get(\"x-tenant\"))\n .map(String::as_str),\n Some(\"abc\")\n );\n }\n}\n"}, "0a25512159d416b664d64eefbf29a5183c60289b": {"t": 1785750251.4581246, "n": 8245, "h": "88f3c7c4fab8c343", "c": "use serde::Deserialize;\nuse std::collections::HashMap;\nuse std::path::Path;\n\n/// A single provider entry from the `providers.toml` catalog.\n#[derive(Debug, Clone, Deserialize)]\npub struct CatalogEntry {\n pub name: String,\n pub display_name: String,\n pub base_url: String,\n pub api_key_env: String,\n pub compatible_with: String,\n pub default_model: String,\n #[serde(default)]\n pub models: Vec<String>,\n /// Model IDs that accept OpenAI-style `image_url` parts (CustomCore / openai-compat).\n #[serde(default)]\n pub vision_models: Vec<String>,\n}\n\n/// Parsed provider catalog loaded from a TOML file.\n///\n/// The catalog maps provider names to their configuration entries, allowing\n/// the kernel to auto-configure `CustomCore` instances for OpenAI-compatible\n/// providers without requiring manual `--base-url` flags.\n#[derive(Debug, Clone)]\npub struct ProviderCatalog {\n providers: HashMap<String, CatalogEntry>,\n}\n\n/// Top-level TOML structure: `[[provider]]` array.\n#[derive(Debug, Deserialize)]\nstruct CatalogFile {\n #[serde(default)]\n provider: Vec<CatalogEntry>,\n}\n\nimpl ProviderCatalog {\n /// Parse a catalog from a TOML string.\n pub fn parse(toml_str: &str) -> Result<Self, toml::de::Error> {\n let file: CatalogFile = toml::from_str(toml_str)?;\n let mut providers = HashMap::new();\n for entry in file.provider {\n providers.insert(entry.name.clone(), entry);\n }\n Ok(Self { providers })\n }\n\n /// Load a catalog from a file path. Returns an empty catalog if the file\n /// does not exist (this is not an error — the catalog is optional).\n pub fn from_file(path: &Path) -> Result<Self, String> {\n if !path.exists() {\n return Ok(Self::empty());\n }\n let content = std::fs::read_to_string(path).map_err(|e| {\n format!(\n \"Failed to read provider catalog at {}: {}\",\n path.display(),\n e\n )\n })?;\n Self::parse(&content).map_err(|e| format!(\"Failed to parse provider catalog: {}\", e))\n }\n\n /// Create an empty catalog with no providers.\n pub fn empty() -> Self {\n Self {\n providers: HashMap::new(),\n }\n }\n\n /// Look up a provider by name (case-insensitive).\n pub fn lookup(&self, name: &str) -> Option<&CatalogEntry> {\n self.providers\n .get(&name.to_lowercase())\n .or_else(|| self.providers.get(name))\n }\n\n /// Return all catalog entries sorted by name.\n pub fn list(&self) -> Vec<&CatalogEntry> {\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n entries\n }\n\n /// Check if the catalog has no entries.\n pub fn is_empty(&self) -> bool {\n self.providers.is_empty()\n }\n\n /// Number of entries in the catalog.\n pub fn len(&self) -> usize {\n self.providers.len()\n }\n\n /// Return a cloned copy of this catalog (used for snapshot-before-write).\n pub fn clone_inner(&self) -> Self {\n Self {\n providers: self.providers.clone(),\n }\n }\n\n /// Override the base URL for an existing provider. Returns `true` if the\n /// provider was found and updated, `false` if the name is unknown.\n pub fn set_base_url(&mut self, name: &str, url: String) -> bool {\n let key = name.to_lowercase();\n if let Some(entry) = self.providers.get_mut(&key) {\n entry.base_url = url;\n true\n } else if let Some(entry) = self.providers.get_mut(name) {\n entry.base_url = url;\n true\n } else {\n false\n }\n }\n\n /// Serialize the catalog back to TOML and write it to `path`.\n pub fn save_to_file(&self, path: &std::path::Path) -> Result<(), String> {\n let mut lines = String::new();\n // Sort entries for stable output\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n for entry in entries {\n lines.push_str(\"[[provider]]\\n\");\n lines.push_str(&format!(\"name = {:?}\\n\", entry.name));\n lines.push_str(&format!(\"display_name = {:?}\\n\", entry.display_name));\n lines.push_str(&format!(\"base_url = {:?}\\n\", entry.base_url));\n lines.push_str(&format!(\"api_key_env = {:?}\\n\", entry.api_key_env));\n lines.push_str(&format!(\"compatible_with = {:?}\\n\", entry.compatible_with));\n lines.push_str(&format!(\"default_model = {:?}\\n\", entry.default_model));\n if !entry.models.is_empty() {\n let models_str = entry\n .models\n .iter()\n .map(|m| format!(\"{:?}\", m))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"models = [{}]\\n\", models_str));\n }\n if !entry.vision_models.is_empty() {\n let vm_str = entry\n .vision_models\n .iter()\n .map(|m| format!(\"{:?}\", m))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"vision_models = [{}]\\n\", vm_str));\n }\n lines.push('\\n');\n }\n std::fs::write(path, lines).map_err(|e| format!(\"Failed to write provider catalog: {}\", e))\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n const TEST_TOML: &str = r#\"\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\"#;\n\n #[test]\n fn test_parse_catalog() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert_eq!(catalog.len(), 3);\n assert!(!catalog.is_empty());\n }\n\n #[test]\n fn test_lookup_existing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entry = catalog.lookup(\"deepseek\").expect(\"should find deepseek\");\n assert_eq!(entry.display_name, \"DeepSeek\");\n assert_eq!(entry.base_url, \"https://api.deepseek.com\");\n assert_eq!(entry.default_model, \"deepseek-chat\");\n assert_eq!(entry.models.len(), 2);\n }\n\n #[test]\n fn test_lookup_missing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert!(catalog.lookup(\"nonexistent\").is_none());\n }\n\n #[test]\n fn test_list_sorted() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entries = catalog.list();\n assert_eq!(entries.len(), 3);\n assert_eq!(entries[0].name, \"deepseek\");\n assert_eq!(entries[1].name, \"groq\");\n assert_eq!(entries[2].name, \"lmstudio\");\n }\n\n #[test]\n fn test_empty_catalog() {\n let catalog = ProviderCatalog::empty();\n assert!(catalog.is_empty());\n assert_eq!(catalog.len(), 0);\n assert!(catalog.lookup(\"anything\").is_none());\n }\n\n #[test]\n fn test_from_file_nonexistent() {\n let catalog = ProviderCatalog::from_file(Path::new(\"/nonexistent/providers.toml\")).unwrap();\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_empty_provider_list() {\n let catalog = ProviderCatalog::parse(\"\").expect(\"empty string should parse\");\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_no_api_key_env() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let lmstudio = catalog.lookup(\"lmstudio\").expect(\"should find lmstudio\");\n assert_eq!(lmstudio.api_key_env, \"\");\n }\n}\n"}, "4ecc8a8f3cec27d4661dd31041dbcc0eba75e600": {"t": 1785750252.555192, "n": 7736, "h": "bcbe727097e9823d", "c": "use serde::Deserialize;\nuse std::collections::HashMap;\nuse std::path::Path;\n\n/// A single provider entry from the `providers.toml` catalog.\n#[derive(Debug, Clone, Deserialize)]\npub struct CatalogEntry {\n pub name: String,\n pub display_name: String,\n pub base_url: String,\n pub api_key_env: String,\n pub compatible_with: String,\n pub default_model: String,\n #[serde(default)]\n pub models: Vec<String>,\n}\n\n/// Parsed provider catalog loaded from a TOML file.\n///\n/// The catalog maps provider names to their configuration entries, allowing\n/// the kernel to auto-configure `CustomCore` instances for OpenAI-compatible\n/// providers without requiring manual `--base-url` flags.\n#[derive(Debug, Clone)]\npub struct ProviderCatalog {\n providers: HashMap<String, CatalogEntry>,\n}\n\n/// Top-level TOML structure: `[[provider]]` array.\n#[derive(Debug, Deserialize)]\nstruct CatalogFile {\n #[serde(default)]\n provider: Vec<CatalogEntry>,\n}\n\nimpl ProviderCatalog {\n /// Parse a catalog from a TOML string.\n pub fn parse(toml_str: &str) -> Result<Self, toml::de::Error> {\n let file: CatalogFile = toml::from_str(toml_str)?;\n let mut providers = HashMap::new();\n for entry in file.provider {\n providers.insert(entry.name.clone(), entry);\n }\n Ok(Self { providers })\n }\n\n /// Load a catalog from a file path. Returns an empty catalog if the file\n /// does not exist (this is not an error — the catalog is optional).\n pub fn from_file(path: &Path) -> Result<Self, String> {\n if !path.exists() {\n return Ok(Self::empty());\n }\n let content = std::fs::read_to_string(path).map_err(|e| {\n format!(\n \"Failed to read provider catalog at {}: {}\",\n path.display(),\n e\n )\n })?;\n Self::parse(&content).map_err(|e| format!(\"Failed to parse provider catalog: {}\", e))\n }\n\n /// Create an empty catalog with no providers.\n pub fn empty() -> Self {\n Self {\n providers: HashMap::new(),\n }\n }\n\n /// Look up a provider by name (case-insensitive).\n pub fn lookup(&self, name: &str) -> Option<&CatalogEntry> {\n self.providers\n .get(&name.to_lowercase())\n .or_else(|| self.providers.get(name))\n }\n\n /// Return all catalog entries sorted by name.\n pub fn list(&self) -> Vec<&CatalogEntry> {\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n entries\n }\n\n /// Check if the catalog has no entries.\n pub fn is_empty(&self) -> bool {\n self.providers.is_empty()\n }\n\n /// Number of entries in the catalog.\n pub fn len(&self) -> usize {\n self.providers.len()\n }\n\n /// Return a cloned copy of this catalog (used for snapshot-before-write).\n pub fn clone_inner(&self) -> Self {\n Self {\n providers: self.providers.clone(),\n }\n }\n\n /// Override the base URL for an existing provider. Returns `true` if the\n /// provider was found and updated, `false` if the name is unknown.\n pub fn set_base_url(&mut self, name: &str, url: String) -> bool {\n let key = name.to_lowercase();\n if let Some(entry) = self.providers.get_mut(&key) {\n entry.base_url = url;\n true\n } else if let Some(entry) = self.providers.get_mut(name) {\n entry.base_url = url;\n true\n } else {\n false\n }\n }\n\n /// Serialize the catalog back to TOML and write it to `path`.\n pub fn save_to_file(&self, path: &std::path::Path) -> Result<(), String> {\n let mut lines = String::new();\n // Sort entries for stable output\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n for entry in entries {\n lines.push_str(\"[[provider]]\\n\");\n lines.push_str(&format!(\"name = {:?}\\n\", entry.name));\n lines.push_str(&format!(\"display_name = {:?}\\n\", entry.display_name));\n lines.push_str(&format!(\"base_url = {:?}\\n\", entry.base_url));\n lines.push_str(&format!(\"api_key_env = {:?}\\n\", entry.api_key_env));\n lines.push_str(&format!(\"compatible_with = {:?}\\n\", entry.compatible_with));\n lines.push_str(&format!(\"default_model = {:?}\\n\", entry.default_model));\n if !entry.models.is_empty() {\n let models_str = entry\n .models\n .iter()\n .map(|m| format!(\"{:?}\", m))\n .collect::<Vec<_>>()\n .join(\", \");\n lines.push_str(&format!(\"models = [{}]\\n\", models_str));\n }\n lines.push('\\n');\n }\n std::fs::write(path, lines).map_err(|e| format!(\"Failed to write provider catalog: {}\", e))\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n const TEST_TOML: &str = r#\"\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\"#;\n\n #[test]\n fn test_parse_catalog() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert_eq!(catalog.len(), 3);\n assert!(!catalog.is_empty());\n }\n\n #[test]\n fn test_lookup_existing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entry = catalog.lookup(\"deepseek\").expect(\"should find deepseek\");\n assert_eq!(entry.display_name, \"DeepSeek\");\n assert_eq!(entry.base_url, \"https://api.deepseek.com\");\n assert_eq!(entry.default_model, \"deepseek-chat\");\n assert_eq!(entry.models.len(), 2);\n }\n\n #[test]\n fn test_lookup_missing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert!(catalog.lookup(\"nonexistent\").is_none());\n }\n\n #[test]\n fn test_list_sorted() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entries = catalog.list();\n assert_eq!(entries.len(), 3);\n assert_eq!(entries[0].name, \"deepseek\");\n assert_eq!(entries[1].name, \"groq\");\n assert_eq!(entries[2].name, \"lmstudio\");\n }\n\n #[test]\n fn test_empty_catalog() {\n let catalog = ProviderCatalog::empty();\n assert!(catalog.is_empty());\n assert_eq!(catalog.len(), 0);\n assert!(catalog.lookup(\"anything\").is_none());\n }\n\n #[test]\n fn test_from_file_nonexistent() {\n let catalog = ProviderCatalog::from_file(Path::new(\"/nonexistent/providers.toml\")).unwrap();\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_empty_provider_list() {\n let catalog = ProviderCatalog::parse(\"\").expect(\"empty string should parse\");\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_no_api_key_env() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let lmstudio = catalog.lookup(\"lmstudio\").expect(\"should find lmstudio\");\n assert_eq!(lmstudio.api_key_env, \"\");\n }\n}\n"}, "27f58d7e0ad2364678debf5ccf83b1d9f96b56d9": {"t": 1785750253.6235905, "n": 5597, "h": "d30f025f2c9b15fd", "c": "use serde::Deserialize;\nuse std::collections::HashMap;\nuse std::path::Path;\n\n/// A single provider entry from the `providers.toml` catalog.\n#[derive(Debug, Clone, Deserialize)]\npub struct CatalogEntry {\n pub name: String,\n pub display_name: String,\n pub base_url: String,\n pub api_key_env: String,\n pub compatible_with: String,\n pub default_model: String,\n #[serde(default)]\n pub models: Vec<String>,\n}\n\n/// Parsed provider catalog loaded from a TOML file.\n///\n/// The catalog maps provider names to their configuration entries, allowing\n/// the kernel to auto-configure `CustomCore` instances for OpenAI-compatible\n/// providers without requiring manual `--base-url` flags.\n#[derive(Debug, Clone)]\npub struct ProviderCatalog {\n providers: HashMap<String, CatalogEntry>,\n}\n\n/// Top-level TOML structure: `[[provider]]` array.\n#[derive(Debug, Deserialize)]\nstruct CatalogFile {\n #[serde(default)]\n provider: Vec<CatalogEntry>,\n}\n\nimpl ProviderCatalog {\n /// Parse a catalog from a TOML string.\n pub fn parse(toml_str: &str) -> Result<Self, toml::de::Error> {\n let file: CatalogFile = toml::from_str(toml_str)?;\n let mut providers = HashMap::new();\n for entry in file.provider {\n providers.insert(entry.name.clone(), entry);\n }\n Ok(Self { providers })\n }\n\n /// Load a catalog from a file path. Returns an empty catalog if the file\n /// does not exist (this is not an error — the catalog is optional).\n pub fn from_file(path: &Path) -> Result<Self, String> {\n if !path.exists() {\n return Ok(Self::empty());\n }\n let content = std::fs::read_to_string(path).map_err(|e| {\n format!(\n \"Failed to read provider catalog at {}: {}\",\n path.display(),\n e\n )\n })?;\n Self::parse(&content).map_err(|e| format!(\"Failed to parse provider catalog: {}\", e))\n }\n\n /// Create an empty catalog with no providers.\n pub fn empty() -> Self {\n Self {\n providers: HashMap::new(),\n }\n }\n\n /// Look up a provider by name (case-insensitive).\n pub fn lookup(&self, name: &str) -> Option<&CatalogEntry> {\n self.providers\n .get(&name.to_lowercase())\n .or_else(|| self.providers.get(name))\n }\n\n /// Return all catalog entries sorted by name.\n pub fn list(&self) -> Vec<&CatalogEntry> {\n let mut entries: Vec<&CatalogEntry> = self.providers.values().collect();\n entries.sort_by_key(|e| &e.name);\n entries\n }\n\n /// Check if the catalog has no entries.\n pub fn is_empty(&self) -> bool {\n self.providers.is_empty()\n }\n\n /// Number of entries in the catalog.\n pub fn len(&self) -> usize {\n self.providers.len()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n const TEST_TOML: &str = r#\"\n[[provider]]\nname = \"deepseek\"\ndisplay_name = \"DeepSeek\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"DEEPSEEK_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"deepseek-chat\"\nmodels = [\"deepseek-chat\", \"deepseek-coder\"]\n\n[[provider]]\nname = \"groq\"\ndisplay_name = \"Groq\"\nbase_url = \"https://api.groq.com/openai/v1\"\napi_key_env = \"GROQ_API_KEY\"\ncompatible_with = \"openai\"\ndefault_model = \"llama-3.3-70b-versatile\"\nmodels = [\"llama-3.3-70b-versatile\", \"mixtral-8x7b-32768\"]\n\n[[provider]]\nname = \"lmstudio\"\ndisplay_name = \"LM Studio\"\nbase_url = \"http://localhost:1234/v1\"\napi_key_env = \"\"\ncompatible_with = \"openai\"\ndefault_model = \"local\"\nmodels = [\"local\"]\n\"#;\n\n #[test]\n fn test_parse_catalog() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert_eq!(catalog.len(), 3);\n assert!(!catalog.is_empty());\n }\n\n #[test]\n fn test_lookup_existing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entry = catalog.lookup(\"deepseek\").expect(\"should find deepseek\");\n assert_eq!(entry.display_name, \"DeepSeek\");\n assert_eq!(entry.base_url, \"https://api.deepseek.com\");\n assert_eq!(entry.default_model, \"deepseek-chat\");\n assert_eq!(entry.models.len(), 2);\n }\n\n #[test]\n fn test_lookup_missing() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n assert!(catalog.lookup(\"nonexistent\").is_none());\n }\n\n #[test]\n fn test_list_sorted() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let entries = catalog.list();\n assert_eq!(entries.len(), 3);\n assert_eq!(entries[0].name, \"deepseek\");\n assert_eq!(entries[1].name, \"groq\");\n assert_eq!(entries[2].name, \"lmstudio\");\n }\n\n #[test]\n fn test_empty_catalog() {\n let catalog = ProviderCatalog::empty();\n assert!(catalog.is_empty());\n assert_eq!(catalog.len(), 0);\n assert!(catalog.lookup(\"anything\").is_none());\n }\n\n #[test]\n fn test_from_file_nonexistent() {\n let catalog = ProviderCatalog::from_file(Path::new(\"/nonexistent/providers.toml\")).unwrap();\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_empty_provider_list() {\n let catalog = ProviderCatalog::parse(\"\").expect(\"empty string should parse\");\n assert!(catalog.is_empty());\n }\n\n #[test]\n fn test_no_api_key_env() {\n let catalog = ProviderCatalog::parse(TEST_TOML).expect(\"should parse\");\n let lmstudio = catalog.lookup(\"lmstudio\").expect(\"should find lmstudio\");\n assert_eq!(lmstudio.api_key_env, \"\");\n }\n}\n"}}}}