Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 src/components/agents/AgentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Copyright (C) 2026 CrewForm

import { Link } from 'react-router-dom'
import { AlertTriangle } from 'lucide-react'
import { StatusIndicator } from '@/components/ui/StatusIndicator'
import { isKnownModel } from '@/lib/modelValidation'
import type { Agent } from '@/types'

interface AgentCardProps {
Expand Down Expand Up @@ -50,7 +52,10 @@ export function AgentCard({ agent }: AgentCardProps) {

{/* Model badge */}
<div className="flex items-center gap-2">
<span className="inline-flex items-center rounded-md border border-border bg-surface-elevated px-2 py-0.5 text-xs font-medium text-gray-400">
<span className="inline-flex items-center gap-1.5 rounded-md border border-border bg-surface-elevated px-2 py-0.5 text-xs font-medium text-gray-400">
{!isKnownModel(agent.model) && (
<AlertTriangle className="h-3 w-3 text-yellow-400" />
)}
{modelShort}
</span>
</div>
Expand Down
24 changes: 24 additions & 0 deletions src/components/agents/AgentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useState, useMemo } from 'react'
import { AlertCircle, Plus, Pencil, Trash2, X, Plug } from 'lucide-react'
import type { AgentFormData } from '@/lib/agentSchema'
import { agentSchema, MODEL_OPTIONS, BUILT_IN_TOOLS, getActiveModelOptions, mergeModelOptions } from '@/lib/agentSchema'
import { isKnownModel, getStaleSuggestion } from '@/lib/modelValidation'
import { useOpenRouterModels } from '@/hooks/useOpenRouterModels'
import { useOllamaModels } from '@/hooks/useOllamaModels'
import { useCustomTools, useCreateCustomTool, useUpdateCustomTool, useDeleteCustomTool } from '@/hooks/useCustomTools'
Expand Down Expand Up @@ -70,6 +71,16 @@ export function AgentForm({ initialData, onSubmit, onBack, activeProviders, work
])
}, [activeProviders, openRouterModels, ollamaModels])

// Check if current model is recognized in the catalog
const allDynamicModelValues = useMemo(
() => [...openRouterModels, ...ollamaModels].map(m => m.value),
[openRouterModels, ollamaModels],
)
const modelKnown = useMemo(
() => !formData.model || isKnownModel(formData.model, allDynamicModelValues),
[formData.model, allDynamicModelValues],
)

const [tagInput, setTagInput] = useState('')

function updateField<K extends keyof AgentFormData>(key: K, value: AgentFormData[K]) {
Expand Down Expand Up @@ -194,6 +205,19 @@ export function AgentForm({ initialData, onSubmit, onBack, activeProviders, work
</div>
)}
{errors.model && <p className="mt-1 text-xs text-status-error-text">{errors.model}</p>}
{!errors.model && !modelKnown && formData.model && (
<div className="mt-2 flex items-start gap-2 rounded-lg border border-yellow-500/30 bg-yellow-500/5 px-3 py-2">
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-yellow-400" />
<div>
<p className="text-xs font-medium text-yellow-300">
Model &quot;{formData.model}&quot; is not in the current catalog
</p>
<p className="mt-0.5 text-[11px] text-yellow-400/70">
{getStaleSuggestion(formData.model)}
</p>
</div>
</div>
)}
</div>

{/* Fallback Model selector */}
Expand Down
7 changes: 6 additions & 1 deletion src/components/agents/AgentListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Copyright (C) 2026 CrewForm

import { Link } from 'react-router-dom'
import { AlertTriangle } from 'lucide-react'
import { StatusIndicator } from '@/components/ui/StatusIndicator'
import { isKnownModel } from '@/lib/modelValidation'
import type { Agent } from '@/types'

interface AgentListRowProps {
Expand Down Expand Up @@ -46,7 +48,10 @@ export function AgentListRow({ agent }: AgentListRowProps) {
</div>

{/* Model badge */}
<span className="hidden shrink-0 items-center rounded-md border border-border bg-surface-elevated px-2 py-0.5 text-xs font-medium text-gray-400 sm:inline-flex">
<span className="hidden shrink-0 items-center gap-1.5 rounded-md border border-border bg-surface-elevated px-2 py-0.5 text-xs font-medium text-gray-400 sm:inline-flex">
{!isKnownModel(agent.model) && (
<AlertTriangle className="h-3 w-3 text-yellow-400" />
)}
{modelShort}
</span>

Expand Down
66 changes: 66 additions & 0 deletions src/lib/modelValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 CrewForm

import { MODEL_OPTIONS } from '@/lib/agentSchema'

/**
* Build a set of all known model IDs from the static MODEL_OPTIONS.
* Used to validate whether an agent's model is still available.
*/
function buildStaticModelSet(): Set<string> {
const set = new Set<string>()
for (const group of MODEL_OPTIONS) {
for (const m of group.models) {
set.add(m.value)
}
}
return set
}

const staticModelSet = buildStaticModelSet()

/**
* Check if a model ID is known (exists in the model catalog).
* For dynamic providers like OpenRouter, pass their live model list.
*
* @param model - The model ID stored on the agent
* @param dynamicModels - Optional array of dynamic model values (e.g. from OpenRouter)
* @returns true if the model is recognized, false if it may be stale/invalid
*/
export function isKnownModel(
model: string,
dynamicModels?: string[],
): boolean {
// Static providers (Anthropic, OpenAI, Google, etc.)
if (staticModelSet.has(model)) return true

// Dynamic models (OpenRouter, Ollama, etc.)
if (dynamicModels && dynamicModels.includes(model)) return true

// Models with custom provider prefixes that we don't validate
// (user may have typed a custom model ID intentionally)
// We only flag models from known providers whose list we have
return false
}

/**
* Get a human-readable suggestion for a stale model.
*/
export function getStaleSuggestion(model: string): string {
const lower = model.toLowerCase()

if (lower.includes('gemini-2.0-flash')) {
return 'Try "gemini-2.5-flash" (Google) or search OpenRouter for the latest Gemini models.'
}
if (lower.includes('gpt-4-turbo')) {
return 'Try "gpt-4o" or "gpt-4.1" — GPT-4 Turbo has been superseded.'
}
if (lower.includes('claude-3-opus')) {
return 'Try "claude-opus-4" — Claude 3 Opus has been replaced by Claude Opus 4.'
}
if (lower.includes('claude-3-sonnet') || lower.includes('claude-3.5-sonnet')) {
return 'Try "claude-sonnet-4" — the latest Sonnet model.'
}

return 'This model may have been renamed or deprecated. Select a current model from the dropdown.'
}
Loading