Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/settings/ModelSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { cn } from "@/lib/utils";
import { formatModelDisplayName } from "@/lib/ai-provider-utils";
import { Search, Check, Sparkles, Zap, Brain, Globe, Server, Cpu, MessageSquare, ChevronRight } from "lucide-react";
import { Search, Check, Sparkles, Zap, Brain, Globe, Server, Cpu, MessageSquare, ChevronRight, Rocket } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import Input from "../components/ui/Input";

Expand All @@ -21,6 +21,7 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
ollama: "Ollama",
groq: "Groq",
cohere: "Cohere",
xai: "xAI",
};

// Get icon for provider category
Expand All @@ -39,6 +40,8 @@ const getCategoryIcon = (category: string) => {
return <Cpu className={className} />;
case "Cohere":
return <MessageSquare className={className} />;
case "xAI":
return <Rocket className={className} />;
default:
return <Sparkles className={className} />;
}
Expand Down Expand Up @@ -84,6 +87,8 @@ const categorizeModels = (models: string[]) => {
categoryName = "Groq";
} else if (model.includes("command") || model.includes("cohere")) {
categoryName = "Cohere";
} else if (model.includes("grok")) {
categoryName = "xAI";
} else {
Comment on lines 76 to 92
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

categorizeModels re-implements provider detection heuristics that largely overlap with detectProviderFromModel in lib/ai-provider-utils.ts. This duplication means future provider additions/heuristic tweaks can easily get out of sync between backend validation (model/API key mismatch) and UI grouping. Consider importing and using detectProviderFromModel (plus getProviderDisplayName / the existing mapping) to derive categories instead of maintaining a separate heuristic set here.

Copilot uses AI. Check for mistakes.
Comment on lines 87 to 92
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

There are existing Playwright E2E tests for model categories and model/API-key mismatch, but none appear to cover the new xAI/Grok category or xai- API key mismatch behavior. Please add/extend E2E coverage (ideally conditional like the Ollama check) to validate that Grok models are grouped under an "xAI" category and that xai-prefixed keys are accepted/mismatch-detected correctly.

Copilot uses AI. Check for mistakes.
categoryName = "Other";
}
Expand Down
21 changes: 17 additions & 4 deletions lib/ai-provider-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Utility functions for AI provider detection and model management
*/

export type AIProvider = "openai" | "anthropic" | "gemini" | "ollama" | "groq" | "cohere" | "unknown";
export type AIProvider = "openai" | "anthropic" | "gemini" | "ollama" | "groq" | "cohere" | "xai" | "unknown";

/**
* Detects the AI provider based on the API key format
Expand Down Expand Up @@ -44,6 +44,11 @@ export function detectProviderFromApiKey(apiKey: string | undefined): AIProvider
return "groq";
}

// xAI: starts with "xai-"
if (trimmedKey.startsWith("xai-")) {
return "xai";
}

// Cohere: typically a long alphanumeric string starting with specific patterns
// No reliable prefix detection for Cohere, handled by model selection

Expand All @@ -70,6 +75,8 @@ export function getProviderDisplayName(provider: AIProvider): string {
return "Groq";
case "cohere":
return "Cohere";
case "xai":
return "xAI";
default:
return "Unknown";
}
Expand Down Expand Up @@ -120,6 +127,11 @@ export function getProviderApiKeyInfo(provider: AIProvider): {
url: "https://dashboard.cohere.com/api-keys",
description: "Get your Cohere API key from the Cohere Dashboard",
};
case "xai":
return {
url: "https://console.x.ai/",
description: "Get your xAI API key from the xAI Console",
};
default:
return null;
}
Expand All @@ -140,7 +152,7 @@ export function detectProviderFromModel(model: string): AIProvider {
const doubleSeparatorIndex = model.indexOf("::");
if (doubleSeparatorIndex !== -1) {
const prefix = model.substring(0, doubleSeparatorIndex);
const knownProviders: AIProvider[] = ["openai", "anthropic", "gemini", "ollama", "groq", "cohere"];
const knownProviders: AIProvider[] = ["openai", "anthropic", "gemini", "ollama", "groq", "cohere", "xai"];
const matched = knownProviders.find(p => p === prefix);
if (matched) return matched;
}
Expand All @@ -149,7 +161,7 @@ export function detectProviderFromModel(model: string): AIProvider {
const singleSeparatorIndex = model.indexOf(":");
if (singleSeparatorIndex !== -1) {
const prefix = model.substring(0, singleSeparatorIndex);
const knownProviders: AIProvider[] = ["openai", "anthropic", "gemini", "ollama", "groq", "cohere"];
const knownProviders: AIProvider[] = ["openai", "anthropic", "gemini", "ollama", "groq", "cohere", "xai"];
const matched = knownProviders.find(p => p === prefix);
if (matched) return matched;
Comment on lines 152 to 166
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

detectProviderFromModel duplicates the knownProviders list in two branches ("::" and ":"), and a similar provider list exists again in formatModelDisplayName. Now that more providers (e.g., xai) are being added, this repetition increases the chance of future misses. Consider extracting a single shared constant (e.g., KNOWN_PROVIDERS) and reusing it in both functions/branches to keep provider support in sync.

Copilot uses AI. Check for mistakes.
}
Expand All @@ -161,6 +173,7 @@ export function detectProviderFromModel(model: string): AIProvider {
if (model.includes("llama") || model.includes("mixtral") || model.includes("phi") || model.includes("deepseek")) return "ollama";
if (model.includes("groq")) return "groq";
if (model.includes("command") || model.includes("cohere")) return "cohere";
if (model.includes("grok")) return "xai";

return "unknown";
}
Expand All @@ -185,7 +198,7 @@ export function formatModelDisplayName(modelValue: string): string {
withoutPrefix = modelValue.substring(doubleSepIndex + 2);
} else {
// Remove legacy single-colon provider prefix (e.g., "anthropic:claude-3-5-sonnet")
const knownPrefixes = ["openai", "anthropic", "gemini", "ollama", "groq", "cohere"];
const knownPrefixes = ["openai", "anthropic", "gemini", "ollama", "groq", "cohere", "xai"];
const singleSepIndex = modelValue.indexOf(":");
if (singleSepIndex !== -1) {
const prefix = modelValue.substring(0, singleSepIndex);
Expand Down
Loading