mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
feat(ui): replace hardcoded model lists with dynamic tokenlens API
Add GET /llm/model-suggestions endpoint that maps ProviderType to models.dev provider keys and returns chat model IDs from the tokenlens catalog. Lazy-loads catalog on first request to avoid empty results when engine hasn't started. Frontend ModelCombobox now fetches suggestions via useQuery with 30min cache instead of reading from hardcoded MODEL_SUGGESTIONS constant. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchModels, MODEL_SUGGESTIONS } from '@/services/llmProviderService';
|
||||
import { fetchModels, fetchModelSuggestions } from '@/services/llmProviderService';
|
||||
import type { ProviderType } from '@/services/llmProviderService';
|
||||
|
||||
interface ModelComboboxProps {
|
||||
@@ -42,9 +42,16 @@ export function ModelCombobox({
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Fetch dynamic model suggestions from backend (powered by models.dev)
|
||||
const { data: suggestions = {} } = useQuery({
|
||||
queryKey: ['llm-model-suggestions'],
|
||||
queryFn: fetchModelSuggestions,
|
||||
staleTime: 30 * 60 * 1000, // 30 min cache
|
||||
});
|
||||
|
||||
// Build tagged model list: API > suggestions > custom input
|
||||
const useApiFetched = fetchedModels.length > 0;
|
||||
const suggestionModels = providerType ? MODEL_SUGGESTIONS[providerType] || [] : [];
|
||||
const suggestionModels = providerType ? suggestions[providerType] || [] : [];
|
||||
|
||||
type TaggedModel = { name: string; tag: 'API' | '推荐' | '自定义' };
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ vi.mock('@/services/llmProviderService', async () => {
|
||||
return {
|
||||
...actual,
|
||||
fetchModels: vi.fn(),
|
||||
fetchModelSuggestions: vi.fn().mockResolvedValue({
|
||||
openai_compatible: ['gpt-4o', 'gpt-4o-mini', 'deepseek-chat'],
|
||||
openai_responses: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'],
|
||||
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022'],
|
||||
gemini: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'],
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -31,13 +31,23 @@ export interface TestResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const MODEL_SUGGESTIONS: Record<ProviderType, string[]> = {
|
||||
openai_compatible: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'deepseek-chat', 'qwen-plus'],
|
||||
openai_responses: ['gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o3-mini'],
|
||||
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022', 'claude-opus-4-20250514'],
|
||||
/** Fallback suggestions when API is unavailable (e.g. catalog not loaded yet). */
|
||||
const FALLBACK_SUGGESTIONS: Record<ProviderType, string[]> = {
|
||||
openai_compatible: ['gpt-4o', 'gpt-4o-mini', 'deepseek-chat'],
|
||||
openai_responses: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'],
|
||||
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022'],
|
||||
gemini: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'],
|
||||
};
|
||||
|
||||
export const fetchModelSuggestions = async (): Promise<Record<string, string[]>> => {
|
||||
try {
|
||||
const response = await api.get<Record<string, string[]>>('/llm/model-suggestions');
|
||||
return response.data;
|
||||
} catch {
|
||||
return FALLBACK_SUGGESTIONS;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchProviders = async (): Promise<ProviderDto[]> => {
|
||||
const response = await api.get<ProviderDto[]>('/llm/providers');
|
||||
return response.data;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { secretRepo } from '../db/repositories/secret-repo';
|
||||
import { settingsRepo } from '../db/repositories/settings-repo';
|
||||
import { llmGateway } from '../llm/gateway';
|
||||
import { MODEL_ROLES } from '../llm/types';
|
||||
import { tokenCounter } from '../review/context/token-counter';
|
||||
|
||||
export const llmConfigRouter = new Hono();
|
||||
|
||||
@@ -289,6 +290,39 @@ llmConfigRouter.post('/providers/:id/test', async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Model Suggestions (from models.dev via tokenlens) ───────────────────
|
||||
|
||||
/**
|
||||
* Map our ProviderType to models.dev provider keys.
|
||||
* openai_compatible is special: it aggregates multiple providers since
|
||||
* users often point compatible endpoints at DeepSeek, Qwen, etc.
|
||||
*/
|
||||
const PROVIDER_TYPE_TO_CATALOG_KEYS: Record<string, string[]> = {
|
||||
openai_compatible: ['openai', 'deepseek', 'qwen'],
|
||||
openai_responses: ['openai'],
|
||||
anthropic: ['anthropic'],
|
||||
gemini: ['google'],
|
||||
};
|
||||
|
||||
llmConfigRouter.get('/model-suggestions', async (c) => {
|
||||
// Ensure catalog is loaded (lazy init on first request)
|
||||
if (!tokenCounter.hasCatalog) {
|
||||
await tokenCounter.refreshCatalog();
|
||||
}
|
||||
|
||||
const result: Record<string, string[]> = {};
|
||||
|
||||
for (const [providerType, catalogKeys] of Object.entries(PROVIDER_TYPE_TO_CATALOG_KEYS)) {
|
||||
const models: string[] = [];
|
||||
for (const key of catalogKeys) {
|
||||
models.push(...tokenCounter.getModelSuggestions(key));
|
||||
}
|
||||
result[providerType] = models;
|
||||
}
|
||||
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// ── System Settings ─────────────────────────────────────────────────────
|
||||
|
||||
llmConfigRouter.get('/settings', (c) => {
|
||||
|
||||
Reference in New Issue
Block a user