feat: catalogue dynamique modèles Codex/Claude avec compatibilité CLI locale
Ajout du catalogue enrichi pour les modèles Codex et Claude avec: - Compatibilité estimée avec la version CLI locale détectée - Source d'origine (catalogue/Provider) pour chaque entrée - Support du catalogue Provider API externe - Matrice de compatibilité embarquée dans l'application Frontend: - UI de configuration des modèles avec affichage des états de compatibilité - Suggestions dynamiques avec badges de compatibilité - Messages d'aide contextuels (compatible/unknown/likelyTooRecent) - Alertes non-bloquantes pour les modèles trop récents - Gestion des échecs de catalogue avec saisie manuelle conservée Backend: - Ports CliVersionReader, ProviderModelCatalogue, CompatibilityMatrixSource - Implémentations: ProcessCliVersionReader, HttpProviderModelCatalogue, EmbeddedCompatibilityMatrix - Enrichissement des DTOs avec compatibility, cli_version, warnings - Tests unitaires complets pour le resolver de catalogue
This commit is contained in:
@ -42,7 +42,7 @@ import type {
|
||||
ProjectWorkState,
|
||||
ProjectSystemPermissions,
|
||||
ProfileAvailability,
|
||||
ProfileModelCatalogEntry,
|
||||
ProfileModelCatalog,
|
||||
ResolvedAgentSystemPermissions,
|
||||
SystemPermissionSet,
|
||||
Skill,
|
||||
@ -74,6 +74,7 @@ import type {
|
||||
} from "@/ports";
|
||||
import { normalizeProjectWorkState } from "../workStateNormalization";
|
||||
import { normalizeTurnPage } from "../conversationNormalization";
|
||||
import { normalizeProfileModelCatalog } from "../profileCatalog";
|
||||
import type { HttpInvoker } from "./httpInvoker";
|
||||
|
||||
export class HttpProjectGateway implements ProjectGateway {
|
||||
@ -183,11 +184,11 @@ export class HttpProfileGateway implements ProfileGateway {
|
||||
request: { seedProfileId: input.seedProfileId, name: input.name, model: input.model },
|
||||
});
|
||||
}
|
||||
listClaudeModels(): Promise<ProfileModelCatalogEntry[]> {
|
||||
return this.http.invoke<ProfileModelCatalogEntry[]>("list_claude_models");
|
||||
async listClaudeModels(): Promise<ProfileModelCatalog> {
|
||||
return normalizeProfileModelCatalog(await this.http.invoke<unknown>("list_claude_models"));
|
||||
}
|
||||
listCodexModels(): Promise<ProfileModelCatalogEntry[]> {
|
||||
return this.http.invoke<ProfileModelCatalogEntry[]>("list_codex_models");
|
||||
async listCodexModels(): Promise<ProfileModelCatalog> {
|
||||
return normalizeProfileModelCatalog(await this.http.invoke<unknown>("list_codex_models"));
|
||||
}
|
||||
configureProfiles(profiles: AgentProfile[]): Promise<AgentProfile[]> {
|
||||
return this.http.invoke<AgentProfile[]>("configure_profiles", { request: { profiles } });
|
||||
|
||||
@ -35,6 +35,7 @@ import type {
|
||||
McpToolCatalogue,
|
||||
McpToolPolicy,
|
||||
OpenCodeProviderCatalogEntry,
|
||||
ProfileModelCatalog,
|
||||
ProfileModelCatalogEntry,
|
||||
EffectivePermissions,
|
||||
PairedDevice,
|
||||
@ -1302,6 +1303,8 @@ const MOCK_CLAUDE_MODELS: ProfileModelCatalogEntry[] = [
|
||||
displayName: "Claude Sonnet 5",
|
||||
aliases: ["sonnet"],
|
||||
recommended: true,
|
||||
compatibility: "compatible",
|
||||
source: "catalogue",
|
||||
},
|
||||
{
|
||||
adapter: "claude",
|
||||
@ -1309,6 +1312,8 @@ const MOCK_CLAUDE_MODELS: ProfileModelCatalogEntry[] = [
|
||||
displayName: "Claude Opus 4.8",
|
||||
aliases: ["opus"],
|
||||
recommended: false,
|
||||
compatibility: "unknown",
|
||||
source: "catalogue",
|
||||
},
|
||||
{
|
||||
adapter: "claude",
|
||||
@ -1316,6 +1321,8 @@ const MOCK_CLAUDE_MODELS: ProfileModelCatalogEntry[] = [
|
||||
displayName: "Claude Haiku 4.5",
|
||||
aliases: ["haiku"],
|
||||
recommended: false,
|
||||
compatibility: "likelyTooRecent",
|
||||
source: "provider",
|
||||
},
|
||||
];
|
||||
|
||||
@ -1326,6 +1333,8 @@ const MOCK_CODEX_MODELS: ProfileModelCatalogEntry[] = [
|
||||
displayName: "GPT-5 Codex",
|
||||
aliases: ["codex"],
|
||||
recommended: true,
|
||||
compatibility: "compatible",
|
||||
source: "catalogue",
|
||||
},
|
||||
{
|
||||
adapter: "codex",
|
||||
@ -1333,6 +1342,8 @@ const MOCK_CODEX_MODELS: ProfileModelCatalogEntry[] = [
|
||||
displayName: "GPT-5",
|
||||
aliases: ["general"],
|
||||
recommended: false,
|
||||
compatibility: "unknown",
|
||||
source: "catalogue",
|
||||
},
|
||||
{
|
||||
adapter: "codex",
|
||||
@ -1340,6 +1351,8 @@ const MOCK_CODEX_MODELS: ProfileModelCatalogEntry[] = [
|
||||
displayName: "GPT-5 mini",
|
||||
aliases: ["mini", "fast"],
|
||||
recommended: false,
|
||||
compatibility: "likelyTooRecent",
|
||||
source: "provider",
|
||||
},
|
||||
];
|
||||
|
||||
@ -1420,12 +1433,20 @@ export class MockProfileGateway implements ProfileGateway {
|
||||
return structuredClone(cloned);
|
||||
}
|
||||
|
||||
async listClaudeModels(): Promise<ProfileModelCatalogEntry[]> {
|
||||
return structuredClone(MOCK_CLAUDE_MODELS);
|
||||
async listClaudeModels(): Promise<ProfileModelCatalog> {
|
||||
return {
|
||||
models: structuredClone(MOCK_CLAUDE_MODELS),
|
||||
cliVersion: "2.1.220",
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
async listCodexModels(): Promise<ProfileModelCatalogEntry[]> {
|
||||
return structuredClone(MOCK_CODEX_MODELS);
|
||||
async listCodexModels(): Promise<ProfileModelCatalog> {
|
||||
return {
|
||||
models: structuredClone(MOCK_CODEX_MODELS),
|
||||
cliVersion: "0.145.0",
|
||||
warnings: ["Catalogue provider partiellement estime depuis les donnees locales."],
|
||||
};
|
||||
}
|
||||
|
||||
async cloneOpenCodeProfileFromSeed(
|
||||
|
||||
@ -12,7 +12,7 @@ import type {
|
||||
AgentProfile,
|
||||
FirstRunState,
|
||||
OpenCodeProviderCatalogEntry,
|
||||
ProfileModelCatalogEntry,
|
||||
ProfileModelCatalog,
|
||||
ProfileAvailability,
|
||||
} from "@/domain";
|
||||
import type {
|
||||
@ -21,6 +21,7 @@ import type {
|
||||
ProfileGateway,
|
||||
SaveOpenCodeProviderProfileInput,
|
||||
} from "@/ports";
|
||||
import { normalizeProfileModelCatalog } from "./profileCatalog";
|
||||
|
||||
export class TauriProfileGateway implements ProfileGateway {
|
||||
firstRunState(): Promise<FirstRunState> {
|
||||
@ -59,12 +60,12 @@ export class TauriProfileGateway implements ProfileGateway {
|
||||
});
|
||||
}
|
||||
|
||||
listClaudeModels(): Promise<ProfileModelCatalogEntry[]> {
|
||||
return invoke<ProfileModelCatalogEntry[]>("list_claude_models");
|
||||
async listClaudeModels(): Promise<ProfileModelCatalog> {
|
||||
return normalizeProfileModelCatalog(await invoke<unknown>("list_claude_models"));
|
||||
}
|
||||
|
||||
listCodexModels(): Promise<ProfileModelCatalogEntry[]> {
|
||||
return invoke<ProfileModelCatalogEntry[]>("list_codex_models");
|
||||
async listCodexModels(): Promise<ProfileModelCatalog> {
|
||||
return normalizeProfileModelCatalog(await invoke<unknown>("list_codex_models"));
|
||||
}
|
||||
|
||||
configureProfiles(profiles: AgentProfile[]): Promise<AgentProfile[]> {
|
||||
|
||||
56
frontend/src/adapters/profileCatalog.test.ts
Normal file
56
frontend/src/adapters/profileCatalog.test.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { normalizeProfileModelCatalog } from "./profileCatalog";
|
||||
|
||||
describe("normalizeProfileModelCatalog", () => {
|
||||
it("wraps the legacy bare array response with unknown compatibility", () => {
|
||||
expect(
|
||||
normalizeProfileModelCatalog([
|
||||
{
|
||||
adapter: "codex",
|
||||
modelId: "gpt-5-codex",
|
||||
displayName: "GPT-5 Codex",
|
||||
aliases: ["codex"],
|
||||
recommended: true,
|
||||
},
|
||||
]),
|
||||
).toEqual({
|
||||
models: [
|
||||
{
|
||||
adapter: "codex",
|
||||
modelId: "gpt-5-codex",
|
||||
displayName: "GPT-5 Codex",
|
||||
aliases: ["codex"],
|
||||
recommended: true,
|
||||
compatibility: "unknown",
|
||||
source: "catalogue",
|
||||
},
|
||||
],
|
||||
cliVersion: null,
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults missing enriched fields without dropping warnings", () => {
|
||||
expect(
|
||||
normalizeProfileModelCatalog({
|
||||
models: [{ adapter: "claude", modelId: "claude-sonnet-5" }],
|
||||
warnings: ["version inconnue"],
|
||||
}),
|
||||
).toEqual({
|
||||
models: [
|
||||
{
|
||||
adapter: "claude",
|
||||
modelId: "claude-sonnet-5",
|
||||
displayName: "claude-sonnet-5",
|
||||
aliases: [],
|
||||
recommended: false,
|
||||
compatibility: "unknown",
|
||||
source: "catalogue",
|
||||
},
|
||||
],
|
||||
cliVersion: null,
|
||||
warnings: ["version inconnue"],
|
||||
});
|
||||
});
|
||||
});
|
||||
63
frontend/src/adapters/profileCatalog.ts
Normal file
63
frontend/src/adapters/profileCatalog.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import type {
|
||||
ModelCatalogSource,
|
||||
ModelCompatibility,
|
||||
ProfileModelCatalog,
|
||||
ProfileModelCatalogEntry,
|
||||
} from "@/domain";
|
||||
|
||||
type PartialCatalogEntry = Partial<ProfileModelCatalogEntry> & {
|
||||
adapter?: "claude" | "codex";
|
||||
modelId?: string;
|
||||
displayName?: string;
|
||||
};
|
||||
|
||||
function compatibilityOf(value: unknown): ModelCompatibility {
|
||||
return value === "compatible" ||
|
||||
value === "unknown" ||
|
||||
value === "likelyTooRecent"
|
||||
? value
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
function sourceOf(value: unknown): ModelCatalogSource {
|
||||
return value === "provider" ? "provider" : "catalogue";
|
||||
}
|
||||
|
||||
function normalizeEntry(raw: PartialCatalogEntry): ProfileModelCatalogEntry {
|
||||
const modelId = raw.modelId ?? "";
|
||||
return {
|
||||
adapter: raw.adapter ?? "codex",
|
||||
modelId,
|
||||
displayName: raw.displayName ?? modelId,
|
||||
aliases: Array.isArray(raw.aliases) ? raw.aliases : [],
|
||||
recommended: Boolean(raw.recommended),
|
||||
compatibility: compatibilityOf(raw.compatibility),
|
||||
source: sourceOf(raw.source),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeProfileModelCatalog(raw: unknown): ProfileModelCatalog {
|
||||
if (Array.isArray(raw)) {
|
||||
return {
|
||||
models: raw.map((entry) => normalizeEntry(entry as PartialCatalogEntry)),
|
||||
cliVersion: null,
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
const catalog =
|
||||
raw && typeof raw === "object"
|
||||
? (raw as Partial<ProfileModelCatalog>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
models: Array.isArray(catalog.models)
|
||||
? catalog.models.map((entry) => normalizeEntry(entry as PartialCatalogEntry))
|
||||
: [],
|
||||
cliVersion:
|
||||
typeof catalog.cliVersion === "string" ? catalog.cliVersion : null,
|
||||
warnings: Array.isArray(catalog.warnings)
|
||||
? catalog.warnings.map(String)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user