feat(wave): #119/#122/#131/#132 verts + sprint plugins ESM/persistance #135/#136/#139
État d'intégration confiné à la branche batch. Les tickets #119 (skills → capacités agent découvrables), #122 (override permissions par défaut), #131 (effort par agent/presets) et #132 (outil MCP d'édition du contexte projet) sont verts en périmètre. Le sprint plugins multi-fichiers ESM / persistance plugin-owned (#135/#136/#139) est co-implémenté dans les MÊMES fichiers de câblage (frontend/src/ports/index.ts, backend/src/lib.rs, domain/ports.rs, backend/dto.rs), inséparable sans staging interactif (indisponible ici). Commit unique volontaire : préserve l'état vert QA sans découpe hunk risquée. NON mergé vers develop tant que #137 (QA e2e plugins) n'est pas vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -21,8 +21,8 @@ use domain::ports::{
|
||||
SpawnSpec, StructuredProviderLaunchPolicy, SystemPermissionStore,
|
||||
};
|
||||
use domain::profile::{
|
||||
McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter, CODEX_CODE_MODE_FEATURES_TABLE,
|
||||
CODEX_CODE_MODE_FEATURES_TOML,
|
||||
resolve_effort, EffortSelection, McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter,
|
||||
CODEX_CODE_MODE_FEATURES_TABLE, CODEX_CODE_MODE_FEATURES_TOML,
|
||||
};
|
||||
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
|
||||
use domain::{
|
||||
@ -123,8 +123,9 @@ pub struct InjectedLiveRow {
|
||||
pub struct ResolvedAssignedSkill {
|
||||
/// Agent-facing snapshot metadata.
|
||||
pub snapshot: domain::AssignedSkillSnapshot,
|
||||
/// Full Markdown body, used only on non-MCP profiles that cannot call
|
||||
/// `idea_skill_read`.
|
||||
/// Full Markdown body kept in the effective snapshot so authorized lazy-read
|
||||
/// paths can use the same resolution; convention-file rendering exposes only
|
||||
/// bounded affordances.
|
||||
pub content: MarkdownDoc,
|
||||
}
|
||||
|
||||
@ -140,7 +141,9 @@ pub struct EffectiveAgentContext {
|
||||
pub capabilities: OrchestrationCapabilitySnapshot,
|
||||
/// Compact agent capability affordances resolved by [`ResolveAgentCapabilities`].
|
||||
pub agent_capabilities: Vec<AgentCapability>,
|
||||
/// Resolved assigned skill bodies for fallback non-MCP injection.
|
||||
/// Resolved assigned skill bodies. These are not dumped into provider context
|
||||
/// by default; agents receive compact affordances and load details through the
|
||||
/// active IdeA surface.
|
||||
pub assigned_skills: Vec<ResolvedAssignedSkill>,
|
||||
/// Project-memory recall selected for this launch.
|
||||
pub memory: Vec<MemoryIndexEntry>,
|
||||
@ -281,6 +284,8 @@ pub struct ListAgentsOutput {
|
||||
pub agents: Vec<Agent>,
|
||||
/// Resolved discoverable capabilities per agent.
|
||||
pub capabilities: Vec<ListedAgentCapabilities>,
|
||||
/// The manifest's resolved orchestrator.
|
||||
pub effective_orchestrator: Option<AgentId>,
|
||||
}
|
||||
|
||||
/// Resolved capabilities for one listed agent.
|
||||
@ -301,6 +306,8 @@ pub struct AgentDiscoveryEntry {
|
||||
pub agent: Agent,
|
||||
/// Resolved capability affordances.
|
||||
pub capabilities: Vec<AgentCapability>,
|
||||
/// Whether this agent is the project's current orchestrator.
|
||||
pub is_orchestrator: bool,
|
||||
}
|
||||
|
||||
impl ListAgentsOutput {
|
||||
@ -318,6 +325,7 @@ impl ListAgentsOutput {
|
||||
.map(|entry| entry.capabilities.clone())
|
||||
.unwrap_or_default();
|
||||
AgentDiscoveryEntry {
|
||||
is_orchestrator: self.effective_orchestrator == Some(agent.id),
|
||||
agent,
|
||||
capabilities,
|
||||
}
|
||||
@ -390,6 +398,7 @@ impl ListAgents {
|
||||
Ok(ListAgentsOutput {
|
||||
agents,
|
||||
capabilities,
|
||||
effective_orchestrator: manifest.effective_orchestrator(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -480,6 +489,74 @@ impl UpdateAgentContext {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UpdateAgentEffort
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`UpdateAgentEffort::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdateAgentEffortInput {
|
||||
/// The owning project.
|
||||
pub project: Project,
|
||||
/// The agent whose effort override changes.
|
||||
pub agent_id: AgentId,
|
||||
/// `None` clears the override, falling back to the profile default.
|
||||
pub effort: Option<EffortSelection>,
|
||||
}
|
||||
|
||||
/// Output of [`UpdateAgentEffort::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdateAgentEffortOutput {
|
||||
/// The updated agent.
|
||||
pub agent: Agent,
|
||||
}
|
||||
|
||||
/// Sets or clears an agent's per-agent effort override in the manifest.
|
||||
///
|
||||
/// The change applies at the agent's next launch; live sessions are not mutated.
|
||||
pub struct UpdateAgentEffort {
|
||||
contexts: Arc<dyn AgentContextStore>,
|
||||
}
|
||||
|
||||
impl UpdateAgentEffort {
|
||||
/// Builds the use case.
|
||||
#[must_use]
|
||||
pub fn new(contexts: Arc<dyn AgentContextStore>) -> Self {
|
||||
Self { contexts }
|
||||
}
|
||||
|
||||
/// Executes the update.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] if the agent is unknown to the project,
|
||||
/// - [`AppError::Invalid`] if the resulting manifest is invalid,
|
||||
/// - [`AppError::Store`] on persistence failure.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: UpdateAgentEffortInput,
|
||||
) -> Result<UpdateAgentEffortOutput, AppError> {
|
||||
let mut manifest = self.contexts.load_manifest(&input.project).await?;
|
||||
let entry = manifest
|
||||
.entries
|
||||
.iter_mut()
|
||||
.find(|entry| entry.agent_id == input.agent_id)
|
||||
.ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
|
||||
|
||||
let agent = entry
|
||||
.to_agent()
|
||||
.map_err(|err| AppError::Invalid(err.to_string()))?
|
||||
.with_effort(input.effort);
|
||||
*entry = ManifestEntry::from_agent(&agent);
|
||||
|
||||
let manifest = AgentManifest::new(manifest.version, manifest.entries)
|
||||
.map_err(|err| AppError::Invalid(err.to_string()))?;
|
||||
self.contexts
|
||||
.save_manifest(&input.project, &manifest)
|
||||
.await?;
|
||||
Ok(UpdateAgentEffortOutput { agent })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ChangeAgentProfile
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -2017,6 +2094,12 @@ impl LaunchAgent {
|
||||
network_permission,
|
||||
&input.project.root,
|
||||
);
|
||||
let resolved_effort = resolve_effort(
|
||||
profile.model_reasoning_effort.as_deref(),
|
||||
agent.effort.as_ref(),
|
||||
);
|
||||
let mut launch_profile = profile.clone();
|
||||
launch_profile.model_reasoning_effort = resolved_effort;
|
||||
|
||||
// 5b. ── POINT DE ROUTAGE §17.4 : IA structuré vs terminal brut ──
|
||||
// L'intention est explicite sur le launcher : les cellules humaines peuvent
|
||||
@ -2053,7 +2136,7 @@ impl LaunchAgent {
|
||||
factory.as_ref(),
|
||||
structured,
|
||||
&agent,
|
||||
&profile,
|
||||
&launch_profile,
|
||||
&prepared,
|
||||
&run_dir,
|
||||
&session_plan,
|
||||
@ -2083,7 +2166,7 @@ impl LaunchAgent {
|
||||
// CODEX_HOME isolé. Passer le modèle du profil sur l'argv garantit que le
|
||||
// lancement interactif respecte l'édition IdeA, comme le chemin structuré
|
||||
// le fait déjà dans `CodexExecSession`.
|
||||
append_codex_pty_model_overrides(&profile, &mut spec);
|
||||
append_codex_pty_model_overrides(&launch_profile, &mut spec);
|
||||
|
||||
// 6. Spawn the PTY at the resolved cwd; adopt its session id everywhere.
|
||||
let handle = self.pty.spawn(spec.clone(), size).await?;
|
||||
@ -3440,8 +3523,7 @@ fn append_block(input: &str, block: &str) -> String {
|
||||
/// Composes the convention file IdeA writes into an agent's run directory: an
|
||||
/// absolute project-root header (the agent's cwd is the run dir, *not* the root,
|
||||
/// so it must be told where to work), the IdeA orchestration contract, the
|
||||
/// agent's persona `.md`, then the bodies of its assigned `skills` under a
|
||||
/// `# Skills` section (ARCHITECTURE §14.2).
|
||||
/// agent's persona `.md` (ARCHITECTURE §14.2).
|
||||
///
|
||||
/// A short skill-awareness paragraph is always injected in the orchestration
|
||||
/// block (followed by the auto-memory harvest directive, Lot E1): it explains that
|
||||
@ -3451,18 +3533,12 @@ fn append_block(input: &str, block: &str) -> String {
|
||||
/// protocol). This awareness deliberately does not inject unassigned skill bodies;
|
||||
/// assignment remains the context boundary.
|
||||
///
|
||||
/// On top of that awareness, the assigned skills surface in one of two ways
|
||||
/// depending on the agent's **surface** (feature « skills à la MCP »), always in
|
||||
/// the given (manifest) order — making the output deterministic:
|
||||
/// - **MCP mode** (`mcp_enabled`): a high-altitude `# Skills disponibles` section,
|
||||
/// right after the orchestration block, listing each as
|
||||
/// `**<name>** — <effective description> (<kind>)` (affordances only, *no body*), with
|
||||
/// prose pointing to `idea_skill_read` to load a body on demand. Respects the
|
||||
/// altitude: the capability is exposed, never the skill content.
|
||||
/// - **Non-MCP mode**: the legacy `# Skills` section dumping each body in full
|
||||
/// under a `##` header carrying its name (unchanged — zero regression).
|
||||
/// When `skills` is empty both sections are omitted entirely, so an agent with no
|
||||
/// skills gets exactly the previous document.
|
||||
/// On top of that awareness, assigned skills surface as a high-altitude
|
||||
/// `# Skills disponibles` section right after the orchestration block, listing
|
||||
/// each as `**<name>** — <effective description> (<kind>)` (affordances only,
|
||||
/// *no body*). This bounded surface is shared by MCP and non-MCP profiles; only
|
||||
/// the instruction for loading details is adapted to the active runtime surface.
|
||||
/// When `skills` is empty the section is omitted entirely.
|
||||
///
|
||||
/// The project's `memory` recall (index/hooks, ARCHITECTURE §14.5.4) is appended as
|
||||
/// a `# Mémoire projet` section — one `- [Title](slug.md) — hook (type)` line per
|
||||
@ -3576,22 +3652,28 @@ pub(crate) fn compose_convention_file(
|
||||
out.push_str(memory_awareness());
|
||||
out.push_str("---\n\n");
|
||||
|
||||
// Skills « à la MCP » (feature skill-awareness) : à HAUTE ALTITUDE, juste après
|
||||
// le bloc d'orchestration. On expose les skills assignés comme des **affordances
|
||||
// nommées+décrites** (et NON leur corps complet), à la manière des outils MCP,
|
||||
// pour que l'agent sache qu'ils existent et charge le détail à la demande via
|
||||
// `idea_skill_read`. Réservé au mode MCP (le mode sans MCP conserve l'ancien dump
|
||||
// du corps complet en fin de fichier, plus bas). Omis si zéro skill.
|
||||
if mcp_enabled && !effective.agent_capabilities.is_empty() {
|
||||
// Skills « capability-first » : à HAUTE ALTITUDE, juste après le bloc
|
||||
// d'orchestration. On expose les skills assignés comme des affordances
|
||||
// nommées+décrites (et NON leur corps complet), à la manière des outils MCP.
|
||||
if !effective.agent_capabilities.is_empty() {
|
||||
out.push_str("# Skills disponibles\n\n");
|
||||
out.push_str("Snapshot version: ");
|
||||
out.push_str(&effective.capabilities.version.to_string());
|
||||
out.push_str("\n\n");
|
||||
out.push_str(
|
||||
"Les skills suivants te sont assignés. Pour en exécuter un ou consulter son \
|
||||
détail, appelle l'outil `idea_skill_read(name=…)` — n'improvise pas un \
|
||||
workflow déjà couvert par un skill, charge-le.\n\n",
|
||||
);
|
||||
if mcp_enabled {
|
||||
out.push_str(
|
||||
"Les skills suivants te sont assignés. Pour en exécuter un ou consulter son \
|
||||
détail, appelle l'outil `idea_skill_read(name=…)` — n'improvise pas un \
|
||||
workflow déjà couvert par un skill, charge-le.\n\n",
|
||||
);
|
||||
} else {
|
||||
out.push_str(
|
||||
"Les skills suivants te sont assignés. Pour en exécuter un ou consulter son \
|
||||
détail, lis le fichier `.ideai/skills/md/<skill-id>.md` correspondant au \
|
||||
`skillId` assigné dans le manifeste — n'improvise pas un workflow déjà \
|
||||
couvert par un skill, charge-le.\n\n",
|
||||
);
|
||||
}
|
||||
for skill in &effective.agent_capabilities {
|
||||
out.push_str("**");
|
||||
out.push_str(&skill.name);
|
||||
@ -3616,24 +3698,6 @@ pub(crate) fn compose_convention_file(
|
||||
|
||||
out.push_str(effective.persona.as_str());
|
||||
|
||||
// MODE SANS MCP (exigence zéro régression, décision produit 4.2(b)) : on conserve
|
||||
// l'ancien dump du **corps complet** des skills en fin de fichier. En mode MCP, le
|
||||
// corps n'est PAS injecté ici (l'agent le charge à la demande via `idea_skill_read`,
|
||||
// cf. la section « # Skills disponibles » à haute altitude plus haut).
|
||||
if !effective.assigned_skills.is_empty() && !mcp_enabled {
|
||||
out.push_str("\n\n---\n\n# Skills\n");
|
||||
out.push_str("\nSnapshot version: ");
|
||||
out.push_str(&effective.capabilities.version.to_string());
|
||||
out.push('\n');
|
||||
for skill in &effective.assigned_skills {
|
||||
out.push_str("\n## ");
|
||||
out.push_str(&skill.snapshot.name);
|
||||
out.push_str("\n\n");
|
||||
out.push_str(skill.content.as_str());
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if !effective.memory.is_empty() {
|
||||
out.push_str("\n\n---\n\n# Mémoire projet\n\n");
|
||||
for entry in &effective.memory {
|
||||
@ -3891,7 +3955,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_convention_file_appends_assigned_skills_in_order() {
|
||||
fn compose_convention_file_appends_assigned_skill_affordances_in_order() {
|
||||
let s = |n: u128, name: &str, body: &str| {
|
||||
Skill::new(
|
||||
domain::SkillId::from_uuid(uuid::Uuid::from_u128(n)),
|
||||
@ -3915,28 +3979,28 @@ mod tests {
|
||||
false,
|
||||
);
|
||||
|
||||
// Both skill bodies present, after the persona.
|
||||
assert!(doc.contains("REFAC_BODY"));
|
||||
assert!(doc.contains("REVIEW_BODY"));
|
||||
// Both skill affordances are present, but bodies are not dumped.
|
||||
assert!(doc.contains("**refactor** — REFAC_BODY (workflow)"));
|
||||
assert!(doc.contains("**review** — REVIEW_BODY (workflow)"));
|
||||
assert!(!doc.contains("\n\nREFAC_BODY"));
|
||||
assert!(!doc.contains("\n\nREVIEW_BODY"));
|
||||
let awareness_at = doc.find("**Skills IdeA**").unwrap();
|
||||
let skills_at = doc.find("# Skills disponibles").unwrap();
|
||||
let persona_at = doc.find("# Persona").unwrap();
|
||||
let skills_at = doc.find("\n# Skills\n").unwrap();
|
||||
let refac_at = doc.find("REFAC_BODY").unwrap();
|
||||
let review_at = doc.find("REVIEW_BODY").unwrap();
|
||||
let refac_at = doc.find("**refactor**").unwrap();
|
||||
let review_at = doc.find("**review**").unwrap();
|
||||
assert!(
|
||||
awareness_at < persona_at,
|
||||
"skill awareness belongs to orchestration, before persona"
|
||||
);
|
||||
assert!(
|
||||
persona_at < skills_at && skills_at < refac_at,
|
||||
"assigned skill bodies come under the Skills section after persona"
|
||||
awareness_at < skills_at && skills_at < persona_at,
|
||||
"assigned skill affordances come before persona"
|
||||
);
|
||||
assert!(persona_at < refac_at, "skills come after the persona");
|
||||
// Deterministic order: first assigned skill precedes the second.
|
||||
assert!(refac_at < review_at, "skills emitted in the given order");
|
||||
// Skill names surface as sub-headers.
|
||||
assert!(doc.contains("## refactor"));
|
||||
assert!(doc.contains("## review"));
|
||||
assert!(!doc.contains("## refactor"));
|
||||
assert!(!doc.contains("## review"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -4048,10 +4112,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compose_convention_file_mcp_mode_exposes_skill_affordances_not_bodies() {
|
||||
// MCP mode (feature « skills à la MCP »): a high-altitude `# Skills disponibles`
|
||||
fn compose_convention_file_exposes_skill_affordances_not_bodies() {
|
||||
// Both surfaces: a high-altitude `# Skills disponibles`
|
||||
// section after the Orchestration block, listing `**name** — description`
|
||||
// affordances and pointing to `idea_skill_read`, WITHOUT dumping the bodies.
|
||||
// affordances, WITHOUT dumping the bodies.
|
||||
let s = |n: u128, name: &str, desc: Option<&str>, body: &str| {
|
||||
Skill::new(
|
||||
domain::SkillId::from_uuid(uuid::Uuid::from_u128(n)),
|
||||
@ -4062,44 +4126,47 @@ mod tests {
|
||||
.unwrap()
|
||||
.with_description(desc.map(str::to_owned))
|
||||
};
|
||||
let doc = compose_convention_file(
|
||||
"/root",
|
||||
"",
|
||||
"# Persona",
|
||||
&[
|
||||
s(1, "refactor", Some("Refactors code"), "REFAC_BODY"),
|
||||
// No explicit description ⇒ effective_description falls back to the
|
||||
// body's first line (heading marker stripped).
|
||||
s(2, "review", None, "# Review skill\n\nREVIEW_BODY"),
|
||||
],
|
||||
&[],
|
||||
None,
|
||||
&[],
|
||||
true, // mcp_enabled
|
||||
);
|
||||
for mcp_enabled in [true, false] {
|
||||
let doc = compose_convention_file(
|
||||
"/root",
|
||||
"",
|
||||
"# Persona",
|
||||
&[
|
||||
s(1, "refactor", Some("Refactors code"), "REFAC_BODY"),
|
||||
// No explicit description ⇒ effective_description falls back to the
|
||||
// body's first line (heading marker stripped).
|
||||
s(2, "review", None, "# Review skill\n\nREVIEW_BODY"),
|
||||
],
|
||||
&[],
|
||||
None,
|
||||
&[],
|
||||
mcp_enabled,
|
||||
);
|
||||
|
||||
// The affordance section is present.
|
||||
assert!(
|
||||
doc.contains("# Skills disponibles"),
|
||||
"MCP skills section present"
|
||||
);
|
||||
assert!(doc.contains("idea_skill_read"), "points to the read tool");
|
||||
// Affordance lines: `**name** — <effective description> (<kind>)`.
|
||||
assert!(doc.contains("**refactor** — Refactors code (workflow)"));
|
||||
assert!(doc.contains("**review** — Review skill (workflow)"));
|
||||
// The full bodies are NOT injected in MCP mode (loaded on demand instead).
|
||||
assert!(!doc.contains("REFAC_BODY"), "no full body in MCP mode");
|
||||
assert!(!doc.contains("REVIEW_BODY"), "no full body in MCP mode");
|
||||
// The legacy `## <name>` body dump headers are absent too.
|
||||
assert!(!doc.contains("## refactor"));
|
||||
assert!(
|
||||
doc.contains("# Skills disponibles"),
|
||||
"skills section present (mcp_enabled={mcp_enabled})"
|
||||
);
|
||||
if mcp_enabled {
|
||||
assert!(doc.contains("idea_skill_read"), "points to the read tool");
|
||||
} else {
|
||||
assert!(
|
||||
doc.contains(".ideai/skills/md/<skill-id>.md"),
|
||||
"points to the file-protocol read path"
|
||||
);
|
||||
}
|
||||
assert!(doc.contains("**refactor** — Refactors code (workflow)"));
|
||||
assert!(doc.contains("**review** — Review skill (workflow)"));
|
||||
assert!(!doc.contains("REFAC_BODY"), "no full body is injected");
|
||||
assert!(!doc.contains("REVIEW_BODY"), "no full body is injected");
|
||||
assert!(!doc.contains("## refactor"));
|
||||
|
||||
// The section sits at high altitude: after the Orchestration block, before
|
||||
// the persona.
|
||||
let orch_at = doc.find("# Orchestration IdeA").unwrap();
|
||||
let skills_at = doc.find("# Skills disponibles").unwrap();
|
||||
let persona_at = doc.find("# Persona").unwrap();
|
||||
assert!(orch_at < skills_at, "skills come after orchestration");
|
||||
assert!(skills_at < persona_at, "skills come before the persona");
|
||||
let orch_at = doc.find("# Orchestration IdeA").unwrap();
|
||||
let skills_at = doc.find("# Skills disponibles").unwrap();
|
||||
let persona_at = doc.find("# Persona").unwrap();
|
||||
assert!(orch_at < skills_at, "skills come after orchestration");
|
||||
assert!(skills_at < persona_at, "skills come before the persona");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -37,8 +37,9 @@ pub use lifecycle::{
|
||||
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, ListedAgentCapabilities,
|
||||
LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry, ProviderSessionProvider,
|
||||
ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput, StructuredRoutingMode,
|
||||
StructuredSessionDescriptor, UpdateAgentContext, UpdateAgentContextInput,
|
||||
AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||
StructuredSessionDescriptor, UpdateAgentContext, UpdateAgentContextInput, UpdateAgentEffort,
|
||||
UpdateAgentEffortInput, UpdateAgentEffortOutput, AGENT_MEMORY_RECALL_BUDGET,
|
||||
DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||
};
|
||||
pub use model_catalogue::{
|
||||
claude_model_catalogue, codex_model_catalogue, ListClaudeModels, ListClaudeModelsOutput,
|
||||
|
||||
Reference in New Issue
Block a user