Adding reasoning stream and config
This commit is contained in:
@@ -66,8 +66,8 @@ cass check
|
||||
|
||||
- Type `/`: show command autocomplete, including command arguments like `/model <model>` and `/new`
|
||||
- `Up`/`Down`: move through an autocomplete menu
|
||||
- `Tab`/`Enter`: fill autocomplete selection
|
||||
- `Enter`: send message / run command when no autocomplete menu is open
|
||||
- `Enter`: fill autocomplete selection when a menu is open; otherwise send message / run command
|
||||
- `Tab`: cycle reasoning effort (`off` → `low` → `medium` → `high`; required-reasoning models skip `off`)
|
||||
- `Ctrl-J`: insert newline
|
||||
- `Shift-Tab`: toggle read-only/full-access mode while idle
|
||||
- `Ctrl-O`: toggle compact/full tool output display
|
||||
@@ -94,7 +94,7 @@ Resume this chat with: cass --resume <id>
|
||||
|
||||
Tool calls are shown compactly by default; press `Ctrl-O` to expand full tool output.
|
||||
|
||||
Reasoning is hidden by default unless `show_reasoning` is enabled; press `Ctrl-Shift-R` to toggle it. When providers stream reasoning fields, Cass persists that reasoning and sends it back in future model context using the provider's reasoning field, such as `reasoning_content` or `reasoning`.
|
||||
Reasoning is hidden by default unless `show_reasoning` is enabled; press `Ctrl-Shift-R` to toggle it. Press `Tab` to choose the reasoning effort for future turns. Model metadata controls whether reasoning is supported or required and how the effort is sent to the provider. When providers stream reasoning fields, Cass persists that reasoning and sends it back in future model context using the provider's reasoning field, such as `reasoning_content` or `reasoning`.
|
||||
|
||||
Read-only mode allows `ls`, `read`, and `grep` within the launch cwd/`--cwd` and the bundled docs directory at `~/.cass/docs`.
|
||||
|
||||
|
||||
+14
-1
@@ -85,7 +85,13 @@ Example:
|
||||
"context_length": 262144,
|
||||
"max_output_tokens": 32768,
|
||||
"supports_tools": true,
|
||||
"supports_streaming": true
|
||||
"supports_streaming": true,
|
||||
"reasoning": {
|
||||
"supported": true,
|
||||
"required": false,
|
||||
"default_effort": "medium",
|
||||
"request_format": "reasoning_effort"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -100,6 +106,13 @@ Fields:
|
||||
- `max_output_tokens`: optional positive integer.
|
||||
- `supports_tools`: optional boolean, defaults to `true`.
|
||||
- `supports_streaming`: optional boolean, defaults to `true`.
|
||||
- `reasoning`: optional object. Defaults to reasoning support enabled with medium effort for model entries.
|
||||
- `supported`: optional boolean, defaults to `true`. Set to `false` for models that do not accept reasoning controls.
|
||||
- `required`: optional boolean, defaults to `false`. If `true`, Cass will not cycle reasoning effort to `off`.
|
||||
- `default_effort`: optional `off`, `low`, `medium`, or `high`; defaults to `medium`. Cannot be `off` when `required` is `true`.
|
||||
- `request_format`: optional `reasoning_effort` or `reasoning_object`; defaults to `reasoning_effort`. `reasoning_effort` sends a top-level `"reasoning_effort": "medium"`; `reasoning_object` sends `"reasoning": { "effort": "medium" }`.
|
||||
|
||||
Reasoning effort is a runtime per-turn setting. Press `Tab` to cycle it while idle. For models with reasoning metadata, the default effort is `medium` unless overridden by `default_effort`; for models without metadata, reasoning starts `off`.
|
||||
|
||||
## Check configuration
|
||||
|
||||
|
||||
+13
-1
@@ -1,5 +1,5 @@
|
||||
use crate::access::AccessMode;
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, ReasoningEffort};
|
||||
use crate::conversation::{now_ts, Conversation, Record, StoredToolCall};
|
||||
use crate::prompt;
|
||||
use crate::providers::openai_compatible::{OpenAiCompatibleProvider, OpenAiCompatibleSettings};
|
||||
@@ -34,6 +34,7 @@ pub struct AgentSettings {
|
||||
pub config: Config,
|
||||
pub cwd: PathBuf,
|
||||
pub mode: AccessMode,
|
||||
pub reasoning_effort: ReasoningEffort,
|
||||
}
|
||||
|
||||
const EMPTY_FINAL_RETRY_PROMPT: &str = "The previous response contained no user-facing text. Provide a concise final user-facing response summarizing the outcome. Do not call tools unless absolutely necessary.";
|
||||
@@ -61,10 +62,21 @@ pub async fn run_turn(
|
||||
return Ok(conversation);
|
||||
}
|
||||
};
|
||||
let reasoning_request_format = settings
|
||||
.config
|
||||
.model_metadata
|
||||
.as_ref()
|
||||
.map(|model| model.reasoning.request_format)
|
||||
.unwrap_or_default();
|
||||
let reasoning_effort = settings
|
||||
.reasoning_effort
|
||||
.clamp_for_model(settings.config.model_metadata.as_ref());
|
||||
let provider = OpenAiCompatibleProvider::new(OpenAiCompatibleSettings {
|
||||
model: settings.config.model.clone(),
|
||||
base_url: settings.config.active_provider.base_url.clone(),
|
||||
api_key,
|
||||
reasoning_effort,
|
||||
reasoning_request_format,
|
||||
});
|
||||
|
||||
let docs_dir = settings.config.docs_dir();
|
||||
|
||||
+45
-5
@@ -1,6 +1,6 @@
|
||||
use crate::agent::{self, AgentEvent, AgentSettings};
|
||||
use crate::cli::{self, Command};
|
||||
use crate::config::{Config, ModelDefinition};
|
||||
use crate::config::{Config, ModelDefinition, ReasoningEffort};
|
||||
use crate::conversation::{self, Conversation};
|
||||
use crate::prompt;
|
||||
use crate::ui::autofill::{AutoFillItem, AutoFillMenu};
|
||||
@@ -96,6 +96,7 @@ async fn run_tui(
|
||||
let mut status = String::new();
|
||||
let mut show_full_tools = false;
|
||||
let mut show_reasoning = config.show_reasoning;
|
||||
let mut reasoning_effort = ReasoningEffort::default_for_model(config.model_metadata.as_ref());
|
||||
let mut scroll: u16 = 0;
|
||||
let mut last_ctrl_c: Option<Instant> = None;
|
||||
let mut handle: Option<JoinHandle<Result<Conversation>>> = None;
|
||||
@@ -197,6 +198,7 @@ async fn run_tui(
|
||||
busy: handle.is_some(),
|
||||
show_full_tools,
|
||||
show_reasoning,
|
||||
reasoning_effort,
|
||||
scroll,
|
||||
autofill: autofill.as_ref(),
|
||||
},
|
||||
@@ -232,10 +234,22 @@ async fn run_tui(
|
||||
}
|
||||
}
|
||||
(KeyCode::Tab, _) => {
|
||||
if let Some(menu) = &autofill {
|
||||
if let Some(next_input) = menu.apply(&input) {
|
||||
input = next_input;
|
||||
autofill_selected = 0;
|
||||
if busy {
|
||||
status = "reasoning effort can be changed when idle".into();
|
||||
} else {
|
||||
let next =
|
||||
reasoning_effort.next_for_model(config.model_metadata.as_ref());
|
||||
if next == reasoning_effort
|
||||
&& next == ReasoningEffort::Off
|
||||
&& !config
|
||||
.model_metadata
|
||||
.as_ref()
|
||||
.is_some_and(|model| model.reasoning.supported)
|
||||
{
|
||||
status = "reasoning unsupported for this model".into();
|
||||
} else {
|
||||
reasoning_effort = next;
|
||||
status.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,6 +388,11 @@ async fn run_tui(
|
||||
status = "model can be changed when idle".into();
|
||||
} else {
|
||||
config.model = model.clone();
|
||||
config.model_metadata =
|
||||
model_metadata_for(&config, &model)?;
|
||||
reasoning_effort = ReasoningEffort::default_for_model(
|
||||
config.model_metadata.as_ref(),
|
||||
);
|
||||
input.clear();
|
||||
autofill_selected = 0;
|
||||
transcript.push(TranscriptBlock {
|
||||
@@ -507,6 +526,7 @@ async fn run_tui(
|
||||
config: config.clone(),
|
||||
cwd: cwd.clone(),
|
||||
mode,
|
||||
reasoning_effort,
|
||||
};
|
||||
let convo = conversation.clone();
|
||||
let tx2 = tx.clone();
|
||||
@@ -912,6 +932,16 @@ fn model_autofill(input: &str, selected: usize, config: &Config) -> Result<Optio
|
||||
}
|
||||
}
|
||||
|
||||
fn model_metadata_for(config: &Config, model_id: &str) -> Result<Option<ModelDefinition>> {
|
||||
let models = crate::config::load_or_create_default_model_registry(&config.root)?;
|
||||
Ok(models
|
||||
.models
|
||||
.iter()
|
||||
.find(|model| model.id == model_id && model.provider == config.provider_id)
|
||||
.cloned()
|
||||
.or_else(|| models.models.into_iter().find(|model| model.id == model_id)))
|
||||
}
|
||||
|
||||
fn model_matches(model: &ModelDefinition, query: &str) -> bool {
|
||||
if query.is_empty() {
|
||||
return true;
|
||||
@@ -947,6 +977,16 @@ fn model_detail(model: &ModelDefinition, current_model: &str) -> String {
|
||||
if !model.supports_streaming {
|
||||
parts.push("no streaming".to_string());
|
||||
}
|
||||
if model.reasoning.supported {
|
||||
let label = if model.reasoning.required {
|
||||
format!("reasoning {} required", model.reasoning.default_effort)
|
||||
} else {
|
||||
format!("reasoning {}", model.reasoning.default_effort)
|
||||
};
|
||||
parts.push(label);
|
||||
} else {
|
||||
parts.push("no reasoning".to_string());
|
||||
}
|
||||
parts.join(" · ")
|
||||
}
|
||||
|
||||
|
||||
+131
@@ -85,6 +85,37 @@ pub struct ModelDefinition {
|
||||
pub supports_tools: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub supports_streaming: bool,
|
||||
#[serde(default)]
|
||||
pub reasoning: ReasoningMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ReasoningMetadata {
|
||||
#[serde(default = "default_true")]
|
||||
pub supported: bool,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(default = "default_reasoning_effort")]
|
||||
pub default_effort: ReasoningEffort,
|
||||
#[serde(default)]
|
||||
pub request_format: ReasoningRequestFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ReasoningEffort {
|
||||
Off,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReasoningRequestFormat {
|
||||
ReasoningEffort,
|
||||
ReasoningObject,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -120,6 +151,93 @@ pub enum ApiKeyReference {
|
||||
Literal,
|
||||
}
|
||||
|
||||
impl Default for ReasoningMetadata {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
supported: true,
|
||||
required: false,
|
||||
default_effort: ReasoningEffort::Medium,
|
||||
request_format: ReasoningRequestFormat::ReasoningEffort,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReasoningRequestFormat {
|
||||
fn default() -> Self {
|
||||
Self::ReasoningEffort
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ReasoningEffort {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
ReasoningEffort::Off => "off",
|
||||
ReasoningEffort::Low => "low",
|
||||
ReasoningEffort::Medium => "medium",
|
||||
ReasoningEffort::High => "high",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ReasoningEffort {
|
||||
pub fn default_for_model(model: Option<&ModelDefinition>) -> Self {
|
||||
let Some(model) = model else {
|
||||
return Self::Off;
|
||||
};
|
||||
if !model.reasoning.supported {
|
||||
return Self::Off;
|
||||
}
|
||||
if model.reasoning.required && model.reasoning.default_effort == Self::Off {
|
||||
Self::Medium
|
||||
} else {
|
||||
model.reasoning.default_effort
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_for_model(self, model: Option<&ModelDefinition>) -> Self {
|
||||
let Some(model) = model else {
|
||||
return Self::Off;
|
||||
};
|
||||
if !model.reasoning.supported {
|
||||
return Self::Off;
|
||||
}
|
||||
let required = model.reasoning.required;
|
||||
match (self, required) {
|
||||
(Self::Off, _) => Self::Low,
|
||||
(Self::Low, _) => Self::Medium,
|
||||
(Self::Medium, _) => Self::High,
|
||||
(Self::High, true) => Self::Low,
|
||||
(Self::High, false) => Self::Off,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamp_for_model(self, model: Option<&ModelDefinition>) -> Self {
|
||||
let Some(model) = model else {
|
||||
return Self::Off;
|
||||
};
|
||||
if !model.reasoning.supported {
|
||||
return Self::Off;
|
||||
}
|
||||
if model.reasoning.required && self == Self::Off {
|
||||
return Self::default_for_model(Some(model));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn request_value(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Off => None,
|
||||
Self::Low => Some("low"),
|
||||
Self::Medium => Some("medium"),
|
||||
Self::High => Some("high"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_reasoning_effort() -> ReasoningEffort {
|
||||
ReasoningEffort::Medium
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let root = cass_root();
|
||||
@@ -335,6 +453,7 @@ pub fn default_model_definition() -> ModelDefinition {
|
||||
max_output_tokens: Some(32_768),
|
||||
supports_tools: true,
|
||||
supports_streaming: true,
|
||||
reasoning: ReasoningMetadata::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,6 +591,18 @@ pub fn validate_registries(
|
||||
model.id
|
||||
));
|
||||
}
|
||||
if model.reasoning.required && !model.reasoning.supported {
|
||||
out.errors.push(format!(
|
||||
"models.json: model `{}` cannot require reasoning when reasoning is unsupported",
|
||||
model.id
|
||||
));
|
||||
}
|
||||
if model.reasoning.required && model.reasoning.default_effort == ReasoningEffort::Off {
|
||||
out.errors.push(format!(
|
||||
"models.json: model `{}` reasoning default_effort cannot be `off` when reasoning is required",
|
||||
model.id
|
||||
));
|
||||
}
|
||||
}
|
||||
for ((provider, id), count) in model_counts {
|
||||
if count > 1 {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::types::{CompletionResult, ModelMessage};
|
||||
use crate::agent::AgentEvent;
|
||||
use crate::config::{ReasoningEffort, ReasoningRequestFormat};
|
||||
use crate::conversation::StoredToolCall;
|
||||
use crate::tools::ToolSpec;
|
||||
use anyhow::{bail, Result};
|
||||
@@ -15,6 +16,8 @@ pub struct OpenAiCompatibleProvider {
|
||||
model: String,
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
reasoning_effort: ReasoningEffort,
|
||||
reasoning_request_format: ReasoningRequestFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -22,6 +25,8 @@ pub struct OpenAiCompatibleSettings {
|
||||
pub model: String,
|
||||
pub base_url: String,
|
||||
pub api_key: String,
|
||||
pub reasoning_effort: ReasoningEffort,
|
||||
pub reasoning_request_format: ReasoningRequestFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -38,6 +43,8 @@ impl OpenAiCompatibleProvider {
|
||||
model: settings.model,
|
||||
base_url: normalize_base_url(&settings.base_url),
|
||||
api_key: settings.api_key,
|
||||
reasoning_effort: settings.reasoning_effort,
|
||||
reasoning_request_format: settings.reasoning_request_format,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +55,17 @@ impl OpenAiCompatibleProvider {
|
||||
tx: &mpsc::UnboundedSender<AgentEvent>,
|
||||
) -> Result<CompletionResult> {
|
||||
let url = chat_url(&self.base_url);
|
||||
let body = json!({
|
||||
let mut body = json!({
|
||||
"model": self.model,
|
||||
"messages": messages_to_openai(messages),
|
||||
"tools": tools_to_openai(tools),
|
||||
"stream": true
|
||||
});
|
||||
apply_reasoning_request(
|
||||
&mut body,
|
||||
self.reasoning_effort,
|
||||
self.reasoning_request_format,
|
||||
);
|
||||
let resp = self
|
||||
.client
|
||||
.post(url)
|
||||
@@ -203,6 +215,30 @@ fn reasoning_delta(delta: &Value) -> Option<(&'static str, &str)> {
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_reasoning_request(
|
||||
body: &mut Value,
|
||||
effort: ReasoningEffort,
|
||||
format: ReasoningRequestFormat,
|
||||
) {
|
||||
let Some(effort) = effort.request_value() else {
|
||||
return;
|
||||
};
|
||||
let Value::Object(obj) = body else {
|
||||
return;
|
||||
};
|
||||
match format {
|
||||
ReasoningRequestFormat::ReasoningEffort => {
|
||||
obj.insert(
|
||||
"reasoning_effort".to_string(),
|
||||
Value::String(effort.to_string()),
|
||||
);
|
||||
}
|
||||
ReasoningRequestFormat::ReasoningObject => {
|
||||
obj.insert("reasoning".to_string(), json!({ "effort": effort }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assistant_message_to_openai(
|
||||
content: String,
|
||||
reasoning: String,
|
||||
|
||||
+4
-1
@@ -1,4 +1,5 @@
|
||||
use crate::access::AccessMode;
|
||||
use crate::config::ReasoningEffort;
|
||||
use crate::ui::autofill::AutoFillMenu;
|
||||
use crate::ui::theme;
|
||||
use ratatui::layout::{Constraint, Direction, Layout};
|
||||
@@ -37,6 +38,7 @@ pub struct RenderState<'a> {
|
||||
pub busy: bool,
|
||||
pub show_full_tools: bool,
|
||||
pub show_reasoning: bool,
|
||||
pub reasoning_effort: ReasoningEffort,
|
||||
pub scroll: u16,
|
||||
pub autofill: Option<&'a AutoFillMenu>,
|
||||
}
|
||||
@@ -354,8 +356,9 @@ fn footer_text(state: &RenderState<'_>) -> String {
|
||||
if state.show_full_tools {
|
||||
parts.push("tools:full".into());
|
||||
}
|
||||
parts.push(format!("reasoning:{}", state.reasoning_effort));
|
||||
if state.show_reasoning {
|
||||
parts.push("reasoning:on".into());
|
||||
parts.push("reasoning:visible".into());
|
||||
}
|
||||
if !state.status.trim().is_empty() {
|
||||
parts.push(state.status.trim().to_string());
|
||||
|
||||
+109
-1
@@ -1,6 +1,6 @@
|
||||
use cassady::access::AccessMode;
|
||||
use cassady::agent::{run_turn, AgentEvent, AgentSettings};
|
||||
use cassady::config::Config;
|
||||
use cassady::config::{Config, ReasoningEffort, ReasoningRequestFormat};
|
||||
use cassady::conversation::{Conversation, Record};
|
||||
use tempfile::tempdir;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -11,6 +11,111 @@ fn sse(body: &str) -> ResponseTemplate {
|
||||
ResponseTemplate::new(200).set_body_raw(body.as_bytes().to_vec(), "text/event-stream")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_effort_is_sent_as_top_level_field() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/chat/completions"))
|
||||
.and(body_string_contains("\"reasoning_effort\":\"high\""))
|
||||
.respond_with(sse(
|
||||
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Done.\"}}]}\r\n\r\ndata: [DONE]\r\n\r\n",
|
||||
))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let root = tempdir().unwrap();
|
||||
let cwd = tempdir().unwrap();
|
||||
let docs = tempdir().unwrap();
|
||||
let config = Config {
|
||||
root: root.path().to_path_buf(),
|
||||
docs_dir: docs.path().to_path_buf(),
|
||||
model: "test-model".into(),
|
||||
active_provider: cassady::config::ResolvedProviderConfig {
|
||||
base_url: server.uri(),
|
||||
api_key: "test-key".into(),
|
||||
..Config::default().active_provider
|
||||
},
|
||||
..Config::default()
|
||||
};
|
||||
let conversation = Conversation::create(
|
||||
&config.conversations_dir(),
|
||||
&config.model,
|
||||
cwd.path(),
|
||||
"base prompt".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let (tx, _rx) = mpsc::unbounded_channel::<AgentEvent>();
|
||||
|
||||
run_turn(
|
||||
conversation,
|
||||
"use high reasoning".into(),
|
||||
AgentSettings {
|
||||
config,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
mode: AccessMode::ReadOnly,
|
||||
reasoning_effort: ReasoningEffort::High,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_effort_supports_reasoning_object_format() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/chat/completions"))
|
||||
.and(body_string_contains("\"reasoning\":{\"effort\":\"low\"}"))
|
||||
.respond_with(sse(
|
||||
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Done.\"}}]}\r\n\r\ndata: [DONE]\r\n\r\n",
|
||||
))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let root = tempdir().unwrap();
|
||||
let cwd = tempdir().unwrap();
|
||||
let docs = tempdir().unwrap();
|
||||
let mut model_metadata = cassady::config::default_model_definition();
|
||||
model_metadata.reasoning.request_format = ReasoningRequestFormat::ReasoningObject;
|
||||
let config = Config {
|
||||
root: root.path().to_path_buf(),
|
||||
docs_dir: docs.path().to_path_buf(),
|
||||
model: "test-model".into(),
|
||||
model_metadata: Some(model_metadata),
|
||||
active_provider: cassady::config::ResolvedProviderConfig {
|
||||
base_url: server.uri(),
|
||||
api_key: "test-key".into(),
|
||||
..Config::default().active_provider
|
||||
},
|
||||
..Config::default()
|
||||
};
|
||||
let conversation = Conversation::create(
|
||||
&config.conversations_dir(),
|
||||
&config.model,
|
||||
cwd.path(),
|
||||
"base prompt".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let (tx, _rx) = mpsc::unbounded_channel::<AgentEvent>();
|
||||
|
||||
run_turn(
|
||||
conversation,
|
||||
"use object reasoning".into(),
|
||||
AgentSettings {
|
||||
config,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
mode: AccessMode::ReadOnly,
|
||||
reasoning_effort: ReasoningEffort::Low,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_is_streamed_persisted_and_sent_back() {
|
||||
let server = MockServer::start().await;
|
||||
@@ -70,6 +175,7 @@ async fn reasoning_is_streamed_persisted_and_sent_back() {
|
||||
config: config.clone(),
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
mode: AccessMode::ReadOnly,
|
||||
reasoning_effort: ReasoningEffort::Off,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
@@ -103,6 +209,7 @@ async fn reasoning_is_streamed_persisted_and_sent_back() {
|
||||
config,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
mode: AccessMode::ReadOnly,
|
||||
reasoning_effort: ReasoningEffort::Off,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
@@ -166,6 +273,7 @@ async fn empty_final_response_is_reprompted_and_persisted() {
|
||||
config,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
mode: AccessMode::ReadOnly,
|
||||
reasoning_effort: ReasoningEffort::Off,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
|
||||
+33
-1
@@ -1,6 +1,9 @@
|
||||
use cassady::check;
|
||||
use cassady::cli::Cli;
|
||||
use cassady::config::{self, Config, ModelsFile, ProviderDefinition, ProvidersFile};
|
||||
use cassady::config::{
|
||||
self, Config, ModelsFile, ProviderDefinition, ProvidersFile, ReasoningEffort,
|
||||
ReasoningRequestFormat,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn cli() -> Cli {
|
||||
@@ -42,6 +45,15 @@ fn default_provider_and_model_files_are_created() {
|
||||
serde_json::from_str(&std::fs::read_to_string(root.path().join("models.json")).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(models.models[0].provider, "fireworks");
|
||||
assert!(models.models[0].reasoning.supported);
|
||||
assert_eq!(
|
||||
models.models[0].reasoning.default_effort,
|
||||
ReasoningEffort::Medium
|
||||
);
|
||||
assert_eq!(
|
||||
models.models[0].reasoning.request_format,
|
||||
ReasoningRequestFormat::ReasoningEffort
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -94,6 +106,26 @@ fn api_key_resolution_supports_env_refs_and_literals() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_defaults_to_supported_medium_for_model_metadata() {
|
||||
let model: config::ModelDefinition = serde_json::from_str(
|
||||
r#"{
|
||||
"id": "test-model",
|
||||
"provider": "test-provider"
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(model.reasoning.supported);
|
||||
assert!(!model.reasoning.required);
|
||||
assert_eq!(model.reasoning.default_effort, ReasoningEffort::Medium);
|
||||
assert_eq!(
|
||||
model.reasoning.request_format,
|
||||
ReasoningRequestFormat::ReasoningEffort
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_duplicate_provider_ids() {
|
||||
let providers = ProvidersFile {
|
||||
|
||||
Reference in New Issue
Block a user