Skip to content

Commit cdcfd98

Browse files
mr-leeabhishekanne
authored andcommitted
feat: add model field support to agent format (aws#2815)
- Add optional 'model' field to Agent struct for specifying model per agent - Update JSON schema and documentation with model field usage - Integrate agent model into model selection priority: 1. CLI argument (--model) 2. Agent's model field (new) 3. User's saved default model 4. System default model - Add proper fallback when agent specifies unavailable model - Extract fallback logic to eliminate code duplication - Include comprehensive unit tests for model field functionality - Maintain backward compatibility with existing agent configurations Co-authored-by: Matt Lee <mr-lee@users.noreply.github.com>
1 parent 9117c91 commit cdcfd98

File tree

4 files changed

+118
-7
lines changed

4 files changed

+118
-7
lines changed

crates/chat-cli/src/cli/agent/mod.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ pub struct Agent {
161161
/// you configure in the mcpServers field in this config
162162
#[serde(default)]
163163
pub use_legacy_mcp_json: bool,
164+
/// The model ID to use for this agent. If not specified, uses the default model.
165+
#[serde(default)]
166+
pub model: Option<String>,
164167
#[serde(skip)]
165168
pub path: Option<PathBuf>,
166169
}
@@ -188,6 +191,7 @@ impl Default for Agent {
188191
hooks: Default::default(),
189192
tools_settings: Default::default(),
190193
use_legacy_mcp_json: true,
194+
model: None,
191195
path: None,
192196
}
193197
}
@@ -1215,6 +1219,7 @@ mod tests {
12151219
resources: Vec::new(),
12161220
hooks: Default::default(),
12171221
use_legacy_mcp_json: false,
1222+
model: None,
12181223
path: None,
12191224
};
12201225

@@ -1285,4 +1290,62 @@ mod tests {
12851290
label
12861291
);
12871292
}
1293+
1294+
#[test]
1295+
fn test_agent_model_field() {
1296+
// Test deserialization with model field
1297+
let agent_json = r#"{
1298+
"name": "test-agent",
1299+
"model": "claude-sonnet-4"
1300+
}"#;
1301+
1302+
let agent: Agent = serde_json::from_str(agent_json).expect("Failed to deserialize agent with model");
1303+
assert_eq!(agent.model, Some("claude-sonnet-4".to_string()));
1304+
1305+
// Test default agent has no model
1306+
let default_agent = Agent::default();
1307+
assert_eq!(default_agent.model, None);
1308+
1309+
// Test serialization includes model field
1310+
let agent_with_model = Agent {
1311+
model: Some("test-model".to_string()),
1312+
..Default::default()
1313+
};
1314+
let serialized = serde_json::to_string(&agent_with_model).expect("Failed to serialize");
1315+
assert!(serialized.contains("\"model\":\"test-model\""));
1316+
}
1317+
1318+
#[test]
1319+
fn test_agent_model_fallback_priority() {
1320+
// Test that agent model is checked and falls back correctly
1321+
let mut agents = Agents::default();
1322+
1323+
// Create agent with unavailable model
1324+
let agent_with_invalid_model = Agent {
1325+
name: "test-agent".to_string(),
1326+
model: Some("unavailable-model".to_string()),
1327+
..Default::default()
1328+
};
1329+
1330+
agents.agents.insert("test-agent".to_string(), agent_with_invalid_model);
1331+
agents.active_idx = "test-agent".to_string();
1332+
1333+
// Verify the agent has the model set
1334+
assert_eq!(
1335+
agents.get_active().and_then(|a| a.model.as_ref()),
1336+
Some(&"unavailable-model".to_string())
1337+
);
1338+
1339+
// Test agent without model
1340+
let agent_without_model = Agent {
1341+
name: "no-model-agent".to_string(),
1342+
model: None,
1343+
..Default::default()
1344+
};
1345+
1346+
agents.agents.insert("no-model-agent".to_string(), agent_without_model);
1347+
agents.active_idx = "no-model-agent".to_string();
1348+
1349+
assert_eq!(agents.get_active().and_then(|a| a.model.as_ref()), None);
1350+
}
12881351
}

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use cli::compact::CompactStrategy;
4444
use cli::model::{
4545
get_available_models,
4646
select_model,
47+
find_model,
4748
};
4849
pub use conversation::ConversationState;
4950
use conversation::TokenWarningLevel;
@@ -141,7 +142,6 @@ use crate::cli::TodoListState;
141142
use crate::cli::agent::Agents;
142143
use crate::cli::chat::cli::SlashCommand;
143144
use crate::cli::chat::cli::editor::open_editor;
144-
use crate::cli::chat::cli::model::find_model;
145145
use crate::cli::chat::cli::prompts::{
146146
GetPromptError,
147147
PromptsSubcommand,
@@ -340,7 +340,17 @@ impl ChatArgs {
340340
// If modelId is specified, verify it exists before starting the chat
341341
// Otherwise, CLI will use a default model when starting chat
342342
let (models, default_model_opt) = get_available_models(os).await?;
343+
// Fallback logic: try user's saved default, then system default
344+
let fallback_model_id = || {
345+
if let Some(saved) = os.database.settings.get_string(Setting::ChatDefaultModel) {
346+
find_model(&models, &saved).map(|m| m.model_id.clone()).or(Some(default_model_opt.model_id.clone()))
347+
} else {
348+
Some(default_model_opt.model_id.clone())
349+
}
350+
};
351+
343352
let model_id: Option<String> = if let Some(requested) = self.model.as_ref() {
353+
// CLI argument takes highest priority
344354
if let Some(m) = find_model(&models, requested) {
345355
Some(m.model_id.clone())
346356
} else {
@@ -351,12 +361,26 @@ impl ChatArgs {
351361
.join(", ");
352362
bail!("Model '{}' does not exist. Available models: {}", requested, available);
353363
}
354-
} else if let Some(saved) = os.database.settings.get_string(Setting::ChatDefaultModel) {
355-
find_model(&models, &saved)
356-
.map(|m| m.model_id.clone())
357-
.or(Some(default_model_opt.model_id.clone()))
364+
} else if let Some(agent_model) = agents.get_active().and_then(|a| a.model.as_ref()) {
365+
// Agent model takes second priority
366+
if let Some(m) = find_model(&models, agent_model) {
367+
Some(m.model_id.clone())
368+
} else {
369+
let _ = execute!(
370+
stderr,
371+
style::SetForegroundColor(Color::Yellow),
372+
style::Print("WARNING: "),
373+
style::SetForegroundColor(Color::Reset),
374+
style::Print("Agent specifies model '"),
375+
style::SetForegroundColor(Color::Cyan),
376+
style::Print(agent_model),
377+
style::SetForegroundColor(Color::Reset),
378+
style::Print("' which is not available. Falling back to configured defaults.\n"),
379+
);
380+
fallback_model_id()
381+
}
358382
} else {
359-
Some(default_model_opt.model_id.clone())
383+
fallback_model_id()
360384
};
361385

362386
let (prompt_request_sender, prompt_request_receiver) = tokio::sync::broadcast::channel::<PromptQuery>(5);

docs/agent-format.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Every agent configuration file can include the following sections:
1515
- [`resources`](#resources-field) — Resources available to the agent.
1616
- [`hooks`](#hooks-field) — Commands run at specific trigger points.
1717
- [`useLegacyMcpJson`](#uselegacymcpjson-field) — Whether to include legacy MCP configuration.
18+
- [`model`](#model-field) — The model ID to use for this agent.
1819

1920
## Name Field
2021

@@ -290,6 +291,20 @@ The `useLegacyMcpJson` field determines whether to include MCP servers defined i
290291

291292
When set to `true`, the agent will have access to all MCP servers defined in the global and local configurations in addition to those defined in the agent's `mcpServers` field.
292293

294+
## Model Field
295+
296+
The `model` field specifies the model ID to use for this agent. If not specified, the agent will use the default model.
297+
298+
```json
299+
{
300+
"model": "claude-sonnet-4"
301+
}
302+
```
303+
304+
The model ID must match one of the available models returned by the Q CLI's model service. You can see available models by using the `/model` command in an active chat session.
305+
306+
If the specified model is not available, the agent will fall back to the default model and display a warning.
307+
293308
## Complete Example
294309

295310
Here's a complete example of an agent configuration file:
@@ -348,6 +363,7 @@ Here's a complete example of an agent configuration file:
348363
}
349364
]
350365
},
351-
"useLegacyMcpJson": true
366+
"useLegacyMcpJson": true,
367+
"model": "claude-sonnet-4"
352368
}
353369
```

schemas/agent-v1.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@
159159
"description": "Whether or not to include the legacy ~/.aws/amazonq/mcp.json in the agent\nYou can reference tools brought in by these servers as just as you would with the servers\nyou configure in the mcpServers field in this config",
160160
"type": "boolean",
161161
"default": false
162+
},
163+
"model": {
164+
"description": "The model ID to use for this agent. If not specified, uses the default model.",
165+
"type": [
166+
"string",
167+
"null"
168+
],
169+
"default": null
162170
}
163171
},
164172
"additionalProperties": false,

0 commit comments

Comments
 (0)