Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions crates/chat-cli/src/cli/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ pub struct Agent {
/// you configure in the mcpServers field in this config
#[serde(default)]
pub use_legacy_mcp_json: bool,
/// The model ID to use for this agent. If not specified, uses the default model.
#[serde(default)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you probably don't need the annotation if the field is already typed with Option.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good call, let me fix that

pub model: Option<String>,
#[serde(skip)]
pub path: Option<PathBuf>,
}
Expand Down Expand Up @@ -188,6 +191,7 @@ impl Default for Agent {
hooks: Default::default(),
tools_settings: Default::default(),
use_legacy_mcp_json: true,
model: None,
path: None,
}
}
Expand Down Expand Up @@ -1215,6 +1219,7 @@ mod tests {
resources: Vec::new(),
hooks: Default::default(),
use_legacy_mcp_json: false,
model: None,
path: None,
};

Expand Down Expand Up @@ -1285,4 +1290,62 @@ mod tests {
label
);
}

#[test]
fn test_agent_model_field() {
// Test deserialization with model field
let agent_json = r#"{
"name": "test-agent",
"model": "claude-sonnet-4"
}"#;

let agent: Agent = serde_json::from_str(agent_json).expect("Failed to deserialize agent with model");
assert_eq!(agent.model, Some("claude-sonnet-4".to_string()));

// Test default agent has no model
let default_agent = Agent::default();
assert_eq!(default_agent.model, None);

// Test serialization includes model field
let agent_with_model = Agent {
model: Some("test-model".to_string()),
..Default::default()
};
let serialized = serde_json::to_string(&agent_with_model).expect("Failed to serialize");
assert!(serialized.contains("\"model\":\"test-model\""));
}

#[test]
fn test_agent_model_fallback_priority() {
// Test that agent model is checked and falls back correctly
let mut agents = Agents::default();

// Create agent with unavailable model
let agent_with_invalid_model = Agent {
name: "test-agent".to_string(),
model: Some("unavailable-model".to_string()),
..Default::default()
};

agents.agents.insert("test-agent".to_string(), agent_with_invalid_model);
agents.active_idx = "test-agent".to_string();

// Verify the agent has the model set
assert_eq!(
agents.get_active().and_then(|a| a.model.as_ref()),
Some(&"unavailable-model".to_string())
);

// Test agent without model
let agent_without_model = Agent {
name: "no-model-agent".to_string(),
model: None,
..Default::default()
};

agents.agents.insert("no-model-agent".to_string(), agent_without_model);
agents.active_idx = "no-model-agent".to_string();

assert_eq!(agents.get_active().and_then(|a| a.model.as_ref()), None);
}
}
36 changes: 30 additions & 6 deletions crates/chat-cli/src/cli/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use cli::compact::CompactStrategy;
use cli::model::{
get_available_models,
select_model,
find_model,
};
pub use conversation::ConversationState;
use conversation::TokenWarningLevel;
Expand Down Expand Up @@ -141,7 +142,6 @@ use crate::cli::TodoListState;
use crate::cli::agent::Agents;
use crate::cli::chat::cli::SlashCommand;
use crate::cli::chat::cli::editor::open_editor;
use crate::cli::chat::cli::model::find_model;
use crate::cli::chat::cli::prompts::{
GetPromptError,
PromptsSubcommand,
Expand Down Expand Up @@ -340,7 +340,17 @@ impl ChatArgs {
// If modelId is specified, verify it exists before starting the chat
// Otherwise, CLI will use a default model when starting chat
let (models, default_model_opt) = get_available_models(os).await?;
// Fallback logic: try user's saved default, then system default
let fallback_model_id = || {
if let Some(saved) = os.database.settings.get_string(Setting::ChatDefaultModel) {
find_model(&models, &saved).map(|m| m.model_id.clone()).or(Some(default_model_opt.model_id.clone()))
} else {
Some(default_model_opt.model_id.clone())
}
};

let model_id: Option<String> = if let Some(requested) = self.model.as_ref() {
// CLI argument takes highest priority
if let Some(m) = find_model(&models, requested) {
Some(m.model_id.clone())
} else {
Expand All @@ -351,12 +361,26 @@ impl ChatArgs {
.join(", ");
bail!("Model '{}' does not exist. Available models: {}", requested, available);
}
} else if let Some(saved) = os.database.settings.get_string(Setting::ChatDefaultModel) {
find_model(&models, &saved)
.map(|m| m.model_id.clone())
.or(Some(default_model_opt.model_id.clone()))
} else if let Some(agent_model) = agents.get_active().and_then(|a| a.model.as_ref()) {
// Agent model takes second priority
if let Some(m) = find_model(&models, agent_model) {
Some(m.model_id.clone())
} else {
let _ = execute!(
stderr,
style::SetForegroundColor(Color::Yellow),
style::Print("WARNING: "),
style::SetForegroundColor(Color::Reset),
style::Print("Agent specifies model '"),
style::SetForegroundColor(Color::Cyan),
style::Print(agent_model),
style::SetForegroundColor(Color::Reset),
style::Print("' which is not available. Falling back to configured defaults.\n"),
);
fallback_model_id()
}
} else {
Some(default_model_opt.model_id.clone())
fallback_model_id()
};

let (prompt_request_sender, prompt_request_receiver) = tokio::sync::broadcast::channel::<PromptQuery>(5);
Expand Down
18 changes: 17 additions & 1 deletion docs/agent-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Every agent configuration file can include the following sections:
- [`resources`](#resources-field) — Resources available to the agent.
- [`hooks`](#hooks-field) — Commands run at specific trigger points.
- [`useLegacyMcpJson`](#uselegacymcpjson-field) — Whether to include legacy MCP configuration.
- [`model`](#model-field) — The model ID to use for this agent.

## Name Field

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

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.

## Model Field

The `model` field specifies the model ID to use for this agent. If not specified, the agent will use the default model.

```json
{
"model": "claude-sonnet-4"
}
```

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.

If the specified model is not available, the agent will fall back to the default model and display a warning.

## Complete Example

Here's a complete example of an agent configuration file:
Expand Down Expand Up @@ -348,6 +363,7 @@ Here's a complete example of an agent configuration file:
}
]
},
"useLegacyMcpJson": true
"useLegacyMcpJson": true,
"model": "claude-sonnet-4"
}
```
8 changes: 8 additions & 0 deletions schemas/agent-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@
"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",
"type": "boolean",
"default": false
},
"model": {
"description": "The model ID to use for this agent. If not specified, uses the default model.",
"type": [
"string",
"null"
],
"default": null
}
},
"additionalProperties": false,
Expand Down