mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-06-03 23:16:45 +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 { useState, useRef, useEffect } from 'react';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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';
|
import type { ProviderType } from '@/services/llmProviderService';
|
||||||
|
|
||||||
interface ModelComboboxProps {
|
interface ModelComboboxProps {
|
||||||
@@ -42,9 +42,16 @@ export function ModelCombobox({
|
|||||||
staleTime: 5 * 60 * 1000,
|
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
|
// Build tagged model list: API > suggestions > custom input
|
||||||
const useApiFetched = fetchedModels.length > 0;
|
const useApiFetched = fetchedModels.length > 0;
|
||||||
const suggestionModels = providerType ? MODEL_SUGGESTIONS[providerType] || [] : [];
|
const suggestionModels = providerType ? suggestions[providerType] || [] : [];
|
||||||
|
|
||||||
type TaggedModel = { name: string; tag: 'API' | '推荐' | '自定义' };
|
type TaggedModel = { name: string; tag: 'API' | '推荐' | '自定义' };
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ vi.mock('@/services/llmProviderService', async () => {
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
fetchModels: vi.fn(),
|
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;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MODEL_SUGGESTIONS: Record<ProviderType, string[]> = {
|
/** Fallback suggestions when API is unavailable (e.g. catalog not loaded yet). */
|
||||||
openai_compatible: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'deepseek-chat', 'qwen-plus'],
|
const FALLBACK_SUGGESTIONS: Record<ProviderType, string[]> = {
|
||||||
openai_responses: ['gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o3-mini'],
|
openai_compatible: ['gpt-4o', 'gpt-4o-mini', 'deepseek-chat'],
|
||||||
anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022', 'claude-opus-4-20250514'],
|
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'],
|
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[]> => {
|
export const fetchProviders = async (): Promise<ProviderDto[]> => {
|
||||||
const response = await api.get<ProviderDto[]>('/llm/providers');
|
const response = await api.get<ProviderDto[]>('/llm/providers');
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { secretRepo } from '../db/repositories/secret-repo';
|
|||||||
import { settingsRepo } from '../db/repositories/settings-repo';
|
import { settingsRepo } from '../db/repositories/settings-repo';
|
||||||
import { llmGateway } from '../llm/gateway';
|
import { llmGateway } from '../llm/gateway';
|
||||||
import { MODEL_ROLES } from '../llm/types';
|
import { MODEL_ROLES } from '../llm/types';
|
||||||
|
import { tokenCounter } from '../review/context/token-counter';
|
||||||
|
|
||||||
export const llmConfigRouter = new Hono();
|
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 ─────────────────────────────────────────────────────
|
// ── System Settings ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
llmConfigRouter.get('/settings', (c) => {
|
llmConfigRouter.get('/settings', (c) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user